mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-16 01:19:19 -04:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62db1de6cc | |||
| 0d5798c0bf | |||
| 808677a612 | |||
| fe41e9d482 | |||
| 0768c4655e | |||
| 1586a26594 | |||
| f026c1422c | |||
| b7fdb595df | |||
| 049ee1ce5e | |||
| e71fbb6d48 | |||
| c9976d69c0 | |||
| 27734409e4 | |||
| e434ce01d3 | |||
| c2f93ae2ad | |||
| 92cc8f553a | |||
| 49d997aec3 | |||
| 67aa9cee23 | |||
| 7ec3d67829 | |||
| 5401ebaede | |||
| 99668cfdce | |||
| b3a012cbdd | |||
| 51b9c726cf | |||
| b4e49d5b32 | |||
| 2b29854589 | |||
| 8ce5038bd8 | |||
| 446510a6a9 | |||
| 49028b6245 | |||
| d595c7e265 | |||
| 17cd4a8c32 | |||
| 9bbf7918de | |||
| 97f502e79a | |||
| ac5e249083 | |||
| 140e796b21 | |||
| dd02cea9e7 | |||
| a8062ea314 | |||
| f47684787a | |||
| 8168f0f0f6 | |||
| 8036440693 | |||
| cf7503687a | |||
| 6f8e1dda15 | |||
| 4dcfd9182c | |||
| d7c0db8cee | |||
| 184da0e42e | |||
| a244d82aba |
@@ -4,7 +4,7 @@ import { tool } from "@opencode-ai/plugin"
|
|||||||
const TEAM = {
|
const TEAM = {
|
||||||
tui: ["kommander", "simonklee"],
|
tui: ["kommander", "simonklee"],
|
||||||
desktop_web: ["Hona", "Brendonovich"],
|
desktop_web: ["Hona", "Brendonovich"],
|
||||||
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton", "starptech"],
|
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"],
|
||||||
inference: ["fwang", "MrMushrooooom", "starptech"],
|
inference: ["fwang", "MrMushrooooom", "starptech"],
|
||||||
windows: ["Hona"],
|
windows: ["Hona"],
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@corvu/drawer": "catalog:",
|
"@corvu/drawer": "catalog:",
|
||||||
"@dnd-kit/abstract": "0.5.0",
|
"@dnd-kit/abstract": "0.5.0",
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"bin": {
|
"bin": {
|
||||||
"lildax": "./bin/lildax.cjs",
|
"lildax": "./bin/lildax.cjs",
|
||||||
},
|
},
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
},
|
},
|
||||||
"packages/codemode": {
|
"packages/codemode": {
|
||||||
"name": "@opencode-ai/codemode",
|
"name": "@opencode-ai/codemode",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "8.15.0",
|
"acorn": "8.15.0",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -266,7 +266,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/support": {
|
"packages/console/support": {
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
@@ -286,7 +286,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -294,7 +294,7 @@
|
|||||||
"@ai-sdk/alibaba": "1.0.17",
|
"@ai-sdk/alibaba": "1.0.17",
|
||||||
"@ai-sdk/amazon-bedrock": "4.0.112",
|
"@ai-sdk/amazon-bedrock": "4.0.112",
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.88",
|
||||||
"@ai-sdk/cerebras": "2.0.41",
|
"@ai-sdk/cerebras": "2.0.41",
|
||||||
"@ai-sdk/cohere": "3.0.27",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "2.0.41",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
@@ -303,7 +303,7 @@
|
|||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.128",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.53",
|
"@ai-sdk/openai": "3.0.84",
|
||||||
"@ai-sdk/openai-compatible": "2.0.41",
|
"@ai-sdk/openai-compatible": "2.0.41",
|
||||||
"@ai-sdk/perplexity": "3.0.26",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
@@ -338,7 +338,7 @@
|
|||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "6.10.0",
|
"gitlab-ai-provider": "6.11.1",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
@@ -380,7 +380,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -434,7 +434,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -448,7 +448,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-sqlite-node": {
|
"packages/effect-sqlite-node": {
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -460,7 +460,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -492,7 +492,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -508,7 +508,7 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "4.0.0-beta.83",
|
"@effect/platform-node": "4.0.0-beta.83",
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||||
@@ -539,7 +539,7 @@
|
|||||||
},
|
},
|
||||||
"packages/llm": {
|
"packages/llm": {
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@smithy/eventstream-codec": "4.2.14",
|
"@smithy/eventstream-codec": "4.2.14",
|
||||||
@@ -558,7 +558,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -569,7 +569,7 @@
|
|||||||
"@ai-sdk/alibaba": "1.0.17",
|
"@ai-sdk/alibaba": "1.0.17",
|
||||||
"@ai-sdk/amazon-bedrock": "4.0.112",
|
"@ai-sdk/amazon-bedrock": "4.0.112",
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.88",
|
||||||
"@ai-sdk/cerebras": "2.0.60",
|
"@ai-sdk/cerebras": "2.0.60",
|
||||||
"@ai-sdk/cohere": "3.0.27",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "2.0.41",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
@@ -578,7 +578,7 @@
|
|||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.128",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.53",
|
"@ai-sdk/openai": "3.0.84",
|
||||||
"@ai-sdk/openai-compatible": "2.0.41",
|
"@ai-sdk/openai-compatible": "2.0.41",
|
||||||
"@ai-sdk/perplexity": "3.0.26",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
@@ -631,7 +631,7 @@
|
|||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "6.10.0",
|
"gitlab-ai-provider": "6.11.1",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
@@ -689,7 +689,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -765,7 +765,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -780,7 +780,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
@@ -795,7 +795,7 @@
|
|||||||
},
|
},
|
||||||
"packages/session-ui": {
|
"packages/session-ui": {
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -839,7 +839,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -852,9 +852,10 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/app": {
|
"packages/stats/app": {
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/stats-core": "workspace:*",
|
"@opencode-ai/stats-core": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
@@ -885,7 +886,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/core": {
|
"packages/stats/core": {
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-athena": "3.933.0",
|
"@aws-sdk/client-athena": "3.933.0",
|
||||||
"@planetscale/database": "1.19.0",
|
"@planetscale/database": "1.19.0",
|
||||||
@@ -904,7 +905,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/server": {
|
"packages/stats/server": {
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
"@aws-sdk/client-firehose": "3.933.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
@@ -945,7 +946,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
@@ -972,7 +973,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
@@ -1023,7 +1024,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -1174,7 +1175,7 @@
|
|||||||
|
|
||||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.82", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-WKKou2wbhGGYV8PSALAPyV2YY4nfCqCPkyBzYtJtDA9yCcIFwsbtkTNgg7bqtLCVzeEsY7wwxRoCWy+EMfrw/A=="],
|
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.82", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-WKKou2wbhGGYV8PSALAPyV2YY4nfCqCPkyBzYtJtDA9yCcIFwsbtkTNgg7bqtLCVzeEsY7wwxRoCWy+EMfrw/A=="],
|
||||||
|
|
||||||
"@ai-sdk/azure": ["@ai-sdk/azure@3.0.49", "", { "dependencies": { "@ai-sdk/openai": "3.0.48", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wskgAL+OmrHG7by/iWIxEBQCEdc1mDudha/UZav46i0auzdFfsDB/k2rXZaC4/3nWSgMZkxr0W3ncyouEGX/eg=="],
|
"@ai-sdk/azure": ["@ai-sdk/azure@3.0.88", "", { "dependencies": { "@ai-sdk/deepseek": "2.0.47", "@ai-sdk/openai": "3.0.84", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RRjZkB1lYplh8dpBarnvkl1j7sYLHsyXua7erL3oNcMK7fHcl4bPO5C7iQhD1O/DqD/zCceDifnege1s+8yEvw=="],
|
||||||
|
|
||||||
"@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kDMEpjaRdRXIUi1EH8WHwLRahyDTYv9SAJnP6VCCeq8X+tVqZbMLCqqxSG5dRknrI65ucjvzQt+FiDKTAa7AHg=="],
|
"@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kDMEpjaRdRXIUi1EH8WHwLRahyDTYv9SAJnP6VCCeq8X+tVqZbMLCqqxSG5dRknrI65ucjvzQt+FiDKTAa7AHg=="],
|
||||||
|
|
||||||
@@ -1184,7 +1185,7 @@
|
|||||||
|
|
||||||
"@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-y6RoOP7DGWmDSiSxrUSt5p18sbz+Ixe5lMVPmdE7x+Tr5rlrzvftyHhjWHfqlAtoYERZTGFbP6tPW1OfQcrb4A=="],
|
"@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-y6RoOP7DGWmDSiSxrUSt5p18sbz+Ixe5lMVPmdE7x+Tr5rlrzvftyHhjWHfqlAtoYERZTGFbP6tPW1OfQcrb4A=="],
|
||||||
|
|
||||||
"@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9DhYurbAvcurOEGN6u2myYDybrrzGfcrkG8hwmFjwTrePW6KCMggm0YxP7e8RkLYcQKqCEMgFlyEB4BM6EmiKg=="],
|
"@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MzcQ321JO8OY+TVLFI81A7cIIuoeLLxrLCDD+8C1E3Ro6UFyfMtRXo9bw9OhTMRSDMo6hgSDOo4Fekz8aJtQYQ=="],
|
||||||
|
|
||||||
"@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EtvsWfGrqx3OhzJdoi82qH+4yzEPPKZr2utyQ+w8cHKoFeg0+8Lou9Z3uixy73WEwz8Z1+AR8QT9fZ64AWGYPA=="],
|
"@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EtvsWfGrqx3OhzJdoi82qH+4yzEPPKZr2utyQ+w8cHKoFeg0+8Lou9Z3uixy73WEwz8Z1+AR8QT9fZ64AWGYPA=="],
|
||||||
|
|
||||||
@@ -3850,7 +3851,7 @@
|
|||||||
|
|
||||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||||
|
|
||||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.10.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": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-oWEZ06rDO6JjB7INHO882wyBAQqCZVHiDHwCs5M+VPmdDj8TzhGXcYesA2CcV5RoI5lfHLKwGp5uKFB62VWpqw=="],
|
"gitlab-ai-provider": ["gitlab-ai-provider@6.11.1", "", { "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": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-SJ6f5qa7P8md6lPrserryER3zerLkrezlnqqYQ2AbvDPpHLbwtbyk0FYJ5kNRcmbI80i/VMcsMBP0YIRdc3ucQ=="],
|
||||||
|
|
||||||
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||||
|
|
||||||
@@ -5654,7 +5655,11 @@
|
|||||||
|
|
||||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||||
|
|
||||||
"@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
"@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="],
|
||||||
|
|
||||||
|
"@ai-sdk/azure/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
|
||||||
|
|
||||||
|
"@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="],
|
||||||
|
|
||||||
"@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
"@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||||
|
|
||||||
@@ -5666,9 +5671,9 @@
|
|||||||
|
|
||||||
"@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
"@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||||
|
|
||||||
"@ai-sdk/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
"@ai-sdk/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
|
||||||
|
|
||||||
"@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
"@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="],
|
||||||
|
|
||||||
"@ai-sdk/elevenlabs/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
"@ai-sdk/elevenlabs/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||||
|
|
||||||
@@ -6020,7 +6025,7 @@
|
|||||||
|
|
||||||
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
||||||
|
|
||||||
"@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
"@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="],
|
||||||
|
|
||||||
"@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
@@ -6162,6 +6167,10 @@
|
|||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
|
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/azure": ["@ai-sdk/azure@3.0.49", "", { "dependencies": { "@ai-sdk/openai": "3.0.48", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wskgAL+OmrHG7by/iWIxEBQCEdc1mDudha/UZav46i0auzdFfsDB/k2rXZaC4/3nWSgMZkxr0W3ncyouEGX/eg=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9DhYurbAvcurOEGN6u2myYDybrrzGfcrkG8hwmFjwTrePW6KCMggm0YxP7e8RkLYcQKqCEMgFlyEB4BM6EmiKg=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
"ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.82", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A0VFMufnVf4wODcT3SPQUUzvYXiIO1VhFuXj9r6z/vP4rlo+QRDPw3WSTchcz93ROQWSfBE3I6Szqz342OHi5w=="],
|
"ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.82", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A0VFMufnVf4wODcT3SPQUUzvYXiIO1VhFuXj9r6z/vP4rlo+QRDPw3WSTchcz93ROQWSfBE3I6Szqz342OHi5w=="],
|
||||||
@@ -6388,7 +6397,7 @@
|
|||||||
|
|
||||||
"opencode/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="],
|
"opencode/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
@@ -6896,6 +6905,10 @@
|
|||||||
|
|
||||||
"@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
"@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
||||||
|
|
||||||
|
"@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
|
||||||
|
|
||||||
|
"@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="],
|
||||||
|
|
||||||
"@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
|
"@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
|
||||||
|
|
||||||
"@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
"@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
||||||
@@ -6966,6 +6979,14 @@
|
|||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"ai-gateway-provider/@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
@@ -7118,6 +7139,10 @@
|
|||||||
|
|
||||||
"opencode/@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA=="],
|
"opencode/@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA=="],
|
||||||
|
|
||||||
|
"opencode/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
|
||||||
|
|
||||||
|
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="],
|
||||||
|
|
||||||
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||||
@@ -7366,6 +7391,8 @@
|
|||||||
|
|
||||||
"@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
"@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
||||||
|
|
||||||
|
"@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
"@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
||||||
|
|
||||||
"@sentry/bundler-plugin-core/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
|
"@sentry/bundler-plugin-core/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
|
||||||
@@ -7382,6 +7409,10 @@
|
|||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||||
@@ -7452,6 +7483,8 @@
|
|||||||
|
|
||||||
"opencode/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"opencode/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||||
|
|
||||||
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-JTtn+wXTXg+yklvIMDLcGFaYhTU6ZrCgKT9JTNEQ3gA=",
|
"x86_64-linux": "sha256-N4zM1zNufSg8DrDWOHWJYgVpn6vDghX/CJ0pym9ItxI=",
|
||||||
"aarch64-linux": "sha256-gXU6zyhvAZrZirkL/PlHdkHtEof/7PVSPCaE34Jnd4U=",
|
"aarch64-linux": "sha256-Votrb6IbVt6OS5pcAlBd3L2btkZHa62Eu3mAFzKSlGM=",
|
||||||
"aarch64-darwin": "sha256-Q0oTG3uzOlD/X2kJingLle529lKFoTpyCW2rHXOZ6iE=",
|
"aarch64-darwin": "sha256-Ofmy6plO4CFt/DoVdyt3Sr2rk6VJhas4zXq3DnvP/6A=",
|
||||||
"x86_64-darwin": "sha256-LINvKHxPibTlJeNzfACQx0x+Yj5oROT6Du3I5AtqqXk="
|
"x86_64-darwin": "sha256-LOeqfqlPbhp1c0Gq56fvKSzve7dvcCwlooTmDMFMznw="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
|
import { expect, test, type Page } from "@playwright/test"
|
||||||
|
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||||
|
import { expectSessionTitle } from "../utils/waits"
|
||||||
|
|
||||||
|
const directory = "C:/OpenCode/FileBrowserSidebar"
|
||||||
|
const projectID = "proj_file_browser_sidebar"
|
||||||
|
const sessionID = "ses_file_browser_sidebar"
|
||||||
|
const title = "File browser sidebar"
|
||||||
|
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||||
|
const files = Array.from({ length: 80 }, (_, index) => `file-${String(index).padStart(2, "0")}.ts`)
|
||||||
|
// Marks the file-browser sidebar DOM node so a remount (fresh node) is detectable.
|
||||||
|
const PROBE = "original"
|
||||||
|
|
||||||
|
test.use({ viewport: { width: 1440, height: 900 } })
|
||||||
|
|
||||||
|
// The file-browser sidebar must stay mounted across preview/pinned file-tab
|
||||||
|
// switches. Remounting resets scroll and filter state.
|
||||||
|
test("keeps the file-browser sidebar mounted when switching file tabs", async ({ page }) => {
|
||||||
|
await setup(page)
|
||||||
|
|
||||||
|
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
|
||||||
|
await expectSessionTitle(page, title)
|
||||||
|
|
||||||
|
const panel = page.locator("#review-panel")
|
||||||
|
await panel.getByRole("button", { name: "Open file" }).click()
|
||||||
|
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
|
||||||
|
|
||||||
|
const sidebar = panel.locator('[data-component="session-review-v2-sidebar-root"]')
|
||||||
|
await expect(sidebar).toBeVisible()
|
||||||
|
await expect(panel.getByRole("button", { name: "file-00.ts" })).toBeVisible()
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "file-00.ts" }).click()
|
||||||
|
await expect(panel.getByRole("tab", { name: "file-00.ts" })).toHaveAttribute("data-selected", "")
|
||||||
|
await expect(panel.getByText("contents:file-00.ts", { exact: true })).toBeVisible()
|
||||||
|
|
||||||
|
const viewport = panel.locator('[data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport')
|
||||||
|
await viewport.hover()
|
||||||
|
await page.mouse.wheel(0, 100_000)
|
||||||
|
await expect
|
||||||
|
.poll(() => viewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
|
||||||
|
.toBeLessThanOrEqual(1)
|
||||||
|
const scrolled = await viewport.evaluate((element) => element.scrollTop)
|
||||||
|
expect(scrolled).toBeGreaterThan(0)
|
||||||
|
await writeProbe(page)
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "file-79.ts" }).click()
|
||||||
|
await expect(panel.getByRole("tab", { name: "file-79.ts" })).toHaveAttribute("data-selected", "")
|
||||||
|
await expect(panel.getByText("contents:file-79.ts", { exact: true })).toBeVisible()
|
||||||
|
expect(await readProbe(page)).toBe(PROBE)
|
||||||
|
await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(scrolled)
|
||||||
|
|
||||||
|
await panel.getByRole("button", { name: "file-78.ts" }).dblclick()
|
||||||
|
await expect(panel.getByRole("tab", { name: "file-78.ts" })).toHaveAttribute("data-selected", "")
|
||||||
|
await panel.getByRole("button", { name: "file-79.ts" }).click()
|
||||||
|
await expect(panel.getByRole("tab", { name: "file-79.ts" })).toHaveAttribute("data-selected", "")
|
||||||
|
await panel.getByRole("tab", { name: "file-78.ts" }).click()
|
||||||
|
await expect(panel.getByRole("tab", { name: "file-78.ts" })).toHaveAttribute("data-selected", "")
|
||||||
|
expect(await readProbe(page)).toBe(PROBE)
|
||||||
|
await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(scrolled)
|
||||||
|
})
|
||||||
|
|
||||||
|
type Probed = HTMLElement & { __e2eProbe?: string }
|
||||||
|
|
||||||
|
async function writeProbe(page: Page) {
|
||||||
|
await page.locator('#review-panel [data-component="session-review-v2-sidebar-root"]').evaluate((el, probe) => {
|
||||||
|
;(el as Probed).__e2eProbe = probe
|
||||||
|
}, PROBE)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readProbe(page: Page) {
|
||||||
|
return page
|
||||||
|
.locator('#review-panel [data-component="session-review-v2-sidebar-root"]')
|
||||||
|
.evaluate((el) => (el as Probed).__e2eProbe)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setup(page: Page) {
|
||||||
|
await mockOpenCodeServer(page, {
|
||||||
|
directory,
|
||||||
|
project: {
|
||||||
|
id: projectID,
|
||||||
|
worktree: directory,
|
||||||
|
vcs: "git",
|
||||||
|
name: "file-browser-sidebar",
|
||||||
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
|
sandboxes: [],
|
||||||
|
},
|
||||||
|
provider: {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
id: "opencode",
|
||||||
|
name: "OpenCode",
|
||||||
|
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connected: ["opencode"],
|
||||||
|
default: { providerID: "opencode", modelID: "test" },
|
||||||
|
},
|
||||||
|
sessions: [
|
||||||
|
{
|
||||||
|
id: sessionID,
|
||||||
|
slug: sessionID,
|
||||||
|
projectID,
|
||||||
|
directory,
|
||||||
|
title,
|
||||||
|
version: "dev",
|
||||||
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vcsDiff: [],
|
||||||
|
fileList: (path) => {
|
||||||
|
if (path) return []
|
||||||
|
return files.map((name) => ({
|
||||||
|
name,
|
||||||
|
path: name,
|
||||||
|
absolute: `${directory}/${name}`,
|
||||||
|
type: "file" as const,
|
||||||
|
ignored: false,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
fileContent: (path) => ({ type: "text", content: `contents:${path}` }),
|
||||||
|
pageMessages: () => ({ items: [] }),
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.addInitScript(
|
||||||
|
({ directory, server, sessionID }) => {
|
||||||
|
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.global.dat:server",
|
||||||
|
JSON.stringify({
|
||||||
|
projects: { local: [{ worktree: directory, expanded: true }] },
|
||||||
|
lastProject: { local: directory },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.global.dat:layout",
|
||||||
|
JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }),
|
||||||
|
)
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.global.dat:review-panel-v2",
|
||||||
|
JSON.stringify({ sidebarOpened: true, sidebarWidth: 240, expandMode: "collapse" }),
|
||||||
|
)
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.window.browser.dat:tabs",
|
||||||
|
JSON.stringify([{ type: "session", server, sessionId: sessionID }]),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ directory, server, sessionID },
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -91,15 +91,17 @@ test("opens and searches project files inline", async ({ page }) => {
|
|||||||
|
|
||||||
const panel = page.locator("#review-panel")
|
const panel = page.locator("#review-panel")
|
||||||
const sidebar = panel.locator('[data-slot="session-review-v2-sidebar"]')
|
const sidebar = panel.locator('[data-slot="session-review-v2-sidebar"]')
|
||||||
|
const sidebarToggle = panel.getByRole("button", { name: "Toggle file tree" })
|
||||||
const contextButton = page.getByRole("button", { name: "View context usage" })
|
const contextButton = page.getByRole("button", { name: "View context usage" })
|
||||||
await contextButton.click()
|
await contextButton.click()
|
||||||
await expect(panel.getByRole("tab", { name: "Context" })).toHaveAttribute("data-selected", "")
|
await expect(panel.getByRole("tab", { name: "Context" })).toHaveAttribute("data-selected", "")
|
||||||
await panel.getByRole("button", { name: "Open file" }).click()
|
await panel.getByRole("button", { name: "Open file" }).click()
|
||||||
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
|
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
|
||||||
|
await expect(sidebarToggle).toBeDisabled()
|
||||||
await expect(sidebar).toBeVisible()
|
await expect(sidebar).toBeVisible()
|
||||||
await contextButton.click()
|
await contextButton.click()
|
||||||
await expect(panel.getByRole("tab", { name: "Context" })).toHaveAttribute("data-selected", "")
|
await expect(panel.getByRole("tab", { name: "Context" })).toHaveAttribute("data-selected", "")
|
||||||
await expect(sidebar).toHaveCount(0)
|
await expect(sidebar).toBeHidden()
|
||||||
await panel.getByRole("button", { name: "Open file" }).click()
|
await panel.getByRole("button", { name: "Open file" }).click()
|
||||||
const filter = panel.getByRole("combobox", { name: "Filter files" })
|
const filter = panel.getByRole("combobox", { name: "Filter files" })
|
||||||
await expect(filter).toBeFocused()
|
await expect(filter).toBeFocused()
|
||||||
@@ -108,6 +110,7 @@ test("opens and searches project files inline", async ({ page }) => {
|
|||||||
|
|
||||||
await panel.getByRole("button", { name: "README.md" }).click()
|
await panel.getByRole("button", { name: "README.md" }).click()
|
||||||
await expect(panel.getByRole("tab", { name: "README.md" })).toHaveAttribute("data-selected", "")
|
await expect(panel.getByRole("tab", { name: "README.md" })).toHaveAttribute("data-selected", "")
|
||||||
|
await expect(sidebarToggle).toBeEnabled()
|
||||||
await expect(panel.getByText("contents:README.md", { exact: true })).toBeVisible()
|
await expect(panel.getByText("contents:README.md", { exact: true })).toBeVisible()
|
||||||
await expect(sidebar).toHaveCount(0)
|
await expect(sidebar).toHaveCount(0)
|
||||||
|
|
||||||
@@ -122,12 +125,17 @@ test("opens and searches project files inline", async ({ page }) => {
|
|||||||
await expect(filter).toHaveAttribute("aria-activedescendant", resultID!)
|
await expect(filter).toHaveAttribute("aria-activedescendant", resultID!)
|
||||||
await filter.press("Enter")
|
await filter.press("Enter")
|
||||||
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveAttribute("data-selected", "")
|
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveAttribute("data-selected", "")
|
||||||
|
await expect(sidebarToggle).toBeEnabled()
|
||||||
await expect(panel.getByText("contents:src/nested.ts", { exact: true })).toBeVisible()
|
await expect(panel.getByText("contents:src/nested.ts", { exact: true })).toBeVisible()
|
||||||
expect(searches).toContainEqual({ query: "nested", dirs: "false", limit: 200 })
|
expect(searches).toContainEqual({ query: "nested", dirs: "false", limit: 200 })
|
||||||
|
|
||||||
await panel.getByRole("button", { name: "Open file" }).click()
|
await panel.getByRole("button", { name: "Open file" }).click()
|
||||||
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveCount(1)
|
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveCount(1)
|
||||||
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
|
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
|
||||||
|
await expect(sidebarToggle).toBeDisabled()
|
||||||
|
await panel.getByRole("tab", { name: /Review/ }).click()
|
||||||
|
await expect(sidebarToggle).toBeEnabled()
|
||||||
|
await panel.getByRole("tab", { name: "Open file" }).click()
|
||||||
await page.keyboard.press("Control+w")
|
await page.keyboard.press("Control+w")
|
||||||
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveCount(0)
|
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveCount(0)
|
||||||
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveAttribute("data-selected", "")
|
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveAttribute("data-selected", "")
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ test("keeps the v2 review pane mounted when switching session tabs in a workspac
|
|||||||
await expectSessionTitle(page, titleA)
|
await expectSessionTitle(page, titleA)
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Toggle review" }).click()
|
await page.getByRole("button", { name: "Toggle review" }).click()
|
||||||
const reviewTab = page.getByRole("tab", { name: /Review/ })
|
const reviewTab = page.locator("#session-side-panel-review-tab")
|
||||||
const reviewTabPanel = page.getByRole("tabpanel", { name: /Review/ })
|
const reviewTabPanel = page.locator("#session-side-panel-review-tabpanel")
|
||||||
await expect(reviewTab).toHaveAttribute("aria-controls", "session-side-panel-review-tabpanel")
|
await expect(reviewTab).toHaveAttribute("aria-controls", "session-side-panel-review-tabpanel")
|
||||||
await expect(reviewTabPanel).toHaveAttribute("id", "session-side-panel-review-tabpanel")
|
await expect(reviewTabPanel).toHaveAttribute("id", "session-side-panel-review-tabpanel")
|
||||||
const review = page.locator('#review-panel [data-component="session-review-v2"]')
|
const review = page.locator('#review-panel [data-component="session-review-v2"]')
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
|
|||||||
await expectTree(page, 8, "git-0.ts")
|
await expectTree(page, 8, "git-0.ts")
|
||||||
|
|
||||||
await selectMode(page, "Git changes", "Branch changes")
|
await selectMode(page, "Git changes", "Branch changes")
|
||||||
await expect(page.getByRole("tab", { name: "Review 2740" })).toBeVisible()
|
await expect(page.locator("#session-side-panel-review-tab")).toHaveText("Files Changed 2740")
|
||||||
await page.keyboard.press("Control+Backquote")
|
await page.keyboard.press("Control+Backquote")
|
||||||
await expect(page.locator("#terminal-panel")).toBeVisible()
|
await expect(page.locator("#terminal-panel")).toBeVisible()
|
||||||
await expectTree(page, 2_773, "action.yml")
|
await expectTree(page, 2_773, "action.yml")
|
||||||
|
|||||||
@@ -152,7 +152,10 @@ test.describe("session timeline projection", () => {
|
|||||||
parentID: "msg_2000_diff_next_user",
|
parentID: "msg_2000_diff_next_user",
|
||||||
created: 1700000011000,
|
created: 1700000011000,
|
||||||
})
|
})
|
||||||
await setupTimeline(page, { messages: [user, assistantMessage(), nextUser, nextAssistant] })
|
await setupTimeline(page, {
|
||||||
|
messages: [user, assistantMessage(), nextUser, nextAssistant],
|
||||||
|
settings: { newLayoutDesigns: false },
|
||||||
|
})
|
||||||
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
|
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
|
||||||
await scroller.evaluate((element) => (element.scrollTop = 0))
|
await scroller.evaluate((element) => (element.scrollTop = 0))
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
|
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
|
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
|
||||||
|
import { Field } from "@opencode-ai/ui/v2/field-v2"
|
||||||
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
|
import { ProjectAvatar, PROJECT_AVATAR_VARIANTS } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
|
import { TextareaV2 } from "@opencode-ai/ui/v2/textarea-v2"
|
||||||
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
|
import { For, Show } from "solid-js"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { getProjectAvatarVariant, type LocalProject } from "@/context/layout"
|
||||||
|
import { ServerConnection } from "@/context/server"
|
||||||
|
import { getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||||
|
import { createEditProjectModel } from "./edit-project"
|
||||||
|
|
||||||
|
export function DialogEditProjectV2(props: { project: LocalProject; server: ServerConnection.Any }) {
|
||||||
|
const language = useLanguage()
|
||||||
|
const model = createEditProjectModel(props)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog fit>
|
||||||
|
<form onSubmit={model.submit} class="contents">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{language.t("dialog.project.edit.title")}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DividerV2 />
|
||||||
|
<DialogBody class="flex max-h-[min(560px,calc(100vh-160px))] w-full flex-col gap-6 overflow-y-auto px-4 pt-4 pb-1">
|
||||||
|
<Field>
|
||||||
|
<Field.Label>{language.t("dialog.project.edit.name")}</Field.Label>
|
||||||
|
<TextInputV2
|
||||||
|
autofocus
|
||||||
|
appearance="large"
|
||||||
|
class="!w-full"
|
||||||
|
value={model.store.name}
|
||||||
|
placeholder={model.folderName()}
|
||||||
|
onInput={(event) => model.setStore("name", event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div class="flex w-full flex-col gap-2">
|
||||||
|
<div class="select-none text-[13px] font-[530] leading-none tracking-[-0.04px] text-v2-text-text-base">
|
||||||
|
{language.t("dialog.project.edit.icon")}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={language.t("dialog.project.edit.icon.alt")}
|
||||||
|
class="relative size-16 shrink-0 cursor-pointer overflow-hidden rounded-[6px] outline outline-1 outline-transparent transition-[background-color,outline-color] focus-visible:outline-v2-border-border-focus"
|
||||||
|
classList={{
|
||||||
|
"bg-v2-overlay-simple-overlay-hover outline-v2-border-border-focus": model.store.dragOver,
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => model.setStore("iconHover", true)}
|
||||||
|
onMouseLeave={() => model.setStore("iconHover", false)}
|
||||||
|
onDrop={model.drop}
|
||||||
|
onDragOver={model.dragOver}
|
||||||
|
onDragLeave={model.dragLeave}
|
||||||
|
onClick={model.iconClick}
|
||||||
|
>
|
||||||
|
<ProjectAvatar
|
||||||
|
fallback={model.store.name || model.defaultName()}
|
||||||
|
src={getProjectAvatarSource(props.project.id, {
|
||||||
|
color: model.store.color,
|
||||||
|
url: props.project.icon?.url,
|
||||||
|
override: model.store.iconOverride,
|
||||||
|
})}
|
||||||
|
variant={getProjectAvatarVariant(model.store.color)}
|
||||||
|
class="!size-16 [&_[data-slot=project-avatar-surface]]:!rounded-[6px] [&_[data-slot=project-avatar-surface]]:!text-[32px]"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="pointer-events-none absolute inset-0 flex items-center justify-center rounded-[6px] bg-v2-background-bg-contrast/80 text-v2-icon-icon-contrast backdrop-blur-[2px] transition-opacity"
|
||||||
|
classList={{
|
||||||
|
"opacity-100": model.store.iconHover,
|
||||||
|
"opacity-0": !model.store.iconHover,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={model.store.iconOverride ? "close" : "outline-share"} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={(element) => {
|
||||||
|
model.setIconInput(element)
|
||||||
|
}}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
onChange={model.inputChange}
|
||||||
|
/>
|
||||||
|
<div class="flex select-none flex-col gap-[6px] text-[11px] font-[440] leading-none tracking-[0.05px] text-v2-text-text-muted">
|
||||||
|
<span>{language.t("dialog.project.edit.icon.hint")}</span>
|
||||||
|
<span>{language.t("dialog.project.edit.icon.recommended")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={!model.store.iconOverride}>
|
||||||
|
<div class="flex w-full flex-col gap-2">
|
||||||
|
<div class="select-none text-[13px] font-[530] leading-none tracking-[-0.04px] text-v2-text-text-base">
|
||||||
|
{language.t("dialog.project.edit.color")}
|
||||||
|
</div>
|
||||||
|
<div class="-ml-1 flex gap-1.5">
|
||||||
|
<For each={PROJECT_AVATAR_VARIANTS}>
|
||||||
|
{(color) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={language.t("dialog.project.edit.color.select", { color })}
|
||||||
|
aria-pressed={getProjectAvatarVariant(model.store.color) === color}
|
||||||
|
class="flex size-8 items-center justify-center rounded-[10px] p-1 outline outline-1 outline-transparent transition-[background-color,outline-color] hover:bg-v2-overlay-simple-overlay-hover focus-visible:outline-v2-border-border-focus"
|
||||||
|
classList={{
|
||||||
|
"bg-v2-overlay-simple-overlay-hover [box-shadow:inset_0_0_0_2px_var(--v2-border-border-focus)]":
|
||||||
|
getProjectAvatarVariant(model.store.color) === color,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (getProjectAvatarVariant(model.store.color) === color && !props.project.icon?.url) return
|
||||||
|
model.setStore(
|
||||||
|
"color",
|
||||||
|
getProjectAvatarVariant(model.store.color) === color ? undefined : color,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProjectAvatar
|
||||||
|
fallback={model.store.name || model.defaultName()}
|
||||||
|
variant={getProjectAvatarVariant(color)}
|
||||||
|
class="!size-6 [&_[data-slot=project-avatar-surface]]:!rounded-[6px]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<Field.Label>{language.t("dialog.project.edit.worktree.startup")}</Field.Label>
|
||||||
|
<Field.Prefix>{language.t("dialog.project.edit.worktree.startup.description")}</Field.Prefix>
|
||||||
|
<TextareaV2
|
||||||
|
class="!w-full [&_[data-slot=textarea-v2-textarea]]:font-mono"
|
||||||
|
rows={3}
|
||||||
|
value={model.store.startup}
|
||||||
|
placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
|
||||||
|
spellcheck={false}
|
||||||
|
onInput={(event) => model.setStore("startup", event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</DialogBody>
|
||||||
|
<DialogFooter>
|
||||||
|
<ButtonV2 type="button" variant="neutral" disabled={model.save.isPending} onClick={model.close}>
|
||||||
|
{language.t("common.cancel")}
|
||||||
|
</ButtonV2>
|
||||||
|
<ButtonV2 type="submit" variant="contrast" disabled={model.save.isPending}>
|
||||||
|
{model.save.isPending ? language.t("common.saving") : language.t("common.save")}
|
||||||
|
</ButtonV2>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,123 +1,32 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { createMemo, For, Show } from "solid-js"
|
import { For, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
|
||||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { getProjectAvatarSource } from "@/pages/layout/helpers"
|
import { getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||||
import { ServerConnection } from "@/context/server"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { useGlobal } from "@/context/global"
|
import { createEditProjectModel } from "./edit-project"
|
||||||
|
|
||||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||||
|
|
||||||
export function DialogEditProject(props: { project: LocalProject; server: ServerConnection.Any }) {
|
export function DialogEditProject(props: { project: LocalProject; server: ServerConnection.Any }) {
|
||||||
const dialog = useDialog()
|
|
||||||
const global = useGlobal()
|
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const serverCtx = createMemo(() => global.ensureServerCtx(props.server))
|
const model = createEditProjectModel(props)
|
||||||
const serverSDK = () => serverCtx().sdk
|
|
||||||
const serverSync = () => serverCtx().sync
|
|
||||||
|
|
||||||
const folderName = createMemo(() => getFilename(props.project.worktree))
|
|
||||||
const defaultName = createMemo(() => props.project.name || folderName())
|
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
|
||||||
name: defaultName(),
|
|
||||||
color: props.project.icon?.color,
|
|
||||||
iconOverride: props.project.icon?.override,
|
|
||||||
startup: props.project.commands?.start ?? "",
|
|
||||||
dragOver: false,
|
|
||||||
iconHover: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
let iconInput: HTMLInputElement | undefined
|
|
||||||
|
|
||||||
function handleFileSelect(file: File) {
|
|
||||||
if (!file.type.startsWith("image/")) return
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
setStore("iconOverride", e.target?.result as string)
|
|
||||||
setStore("iconHover", false)
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDrop(e: DragEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
setStore("dragOver", false)
|
|
||||||
const file = e.dataTransfer?.files[0]
|
|
||||||
if (file) handleFileSelect(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragOver(e: DragEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
setStore("dragOver", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragLeave() {
|
|
||||||
setStore("dragOver", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleInputChange(e: Event) {
|
|
||||||
const input = e.target as HTMLInputElement
|
|
||||||
const file = input.files?.[0]
|
|
||||||
if (file) handleFileSelect(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearIcon() {
|
|
||||||
setStore("iconOverride", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveMutation = useMutation(() => ({
|
|
||||||
mutationFn: async () => {
|
|
||||||
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
|
||||||
const start = store.startup.trim()
|
|
||||||
|
|
||||||
if (props.project.id && props.project.id !== "global") {
|
|
||||||
await serverSDK().client.project.update({
|
|
||||||
projectID: props.project.id,
|
|
||||||
directory: props.project.worktree,
|
|
||||||
name,
|
|
||||||
icon: { color: store.color || "", override: store.iconOverride || "" },
|
|
||||||
commands: { start },
|
|
||||||
})
|
|
||||||
serverSync().project.icon(props.project.worktree, store.iconOverride || undefined)
|
|
||||||
dialog.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
serverSync().project.meta(props.project.worktree, {
|
|
||||||
name,
|
|
||||||
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
|
|
||||||
commands: { start: start || undefined },
|
|
||||||
})
|
|
||||||
dialog.close()
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
function handleSubmit(e: SubmitEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
if (saveMutation.isPending) return
|
|
||||||
saveMutation.mutate()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog title={language.t("dialog.project.edit.title")} class="w-full max-w-[480px] mx-auto">
|
<Dialog title={language.t("dialog.project.edit.title")} class="w-full max-w-[480px] mx-auto">
|
||||||
<form onSubmit={handleSubmit} class="flex flex-col gap-6 p-6 pt-0">
|
<form onSubmit={model.submit} class="flex flex-col gap-6 p-6 pt-0">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<TextField
|
<TextField
|
||||||
autofocus
|
autofocus
|
||||||
type="text"
|
type="text"
|
||||||
label={language.t("dialog.project.edit.name")}
|
label={language.t("dialog.project.edit.name")}
|
||||||
placeholder={folderName()}
|
placeholder={model.folderName()}
|
||||||
value={store.name}
|
value={model.store.name}
|
||||||
onChange={(v) => setStore("name", v)}
|
onChange={(v) => model.setStore("name", v)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
@@ -125,38 +34,32 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
|
|||||||
<div class="flex gap-3 items-start">
|
<div class="flex gap-3 items-start">
|
||||||
<div
|
<div
|
||||||
class="relative"
|
class="relative"
|
||||||
onMouseEnter={() => setStore("iconHover", true)}
|
onMouseEnter={() => model.setStore("iconHover", true)}
|
||||||
onMouseLeave={() => setStore("iconHover", false)}
|
onMouseLeave={() => model.setStore("iconHover", false)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative size-16 rounded-md transition-colors cursor-pointer"
|
class="relative size-16 rounded-md transition-colors cursor-pointer"
|
||||||
classList={{
|
classList={{
|
||||||
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
|
"border-text-interactive-base bg-surface-info-base/20": model.store.dragOver,
|
||||||
"border-border-base hover:border-border-strong": !store.dragOver,
|
"border-border-base hover:border-border-strong": !model.store.dragOver,
|
||||||
"overflow-hidden": !!store.iconOverride,
|
"overflow-hidden": !!model.store.iconOverride,
|
||||||
}}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onClick={() => {
|
|
||||||
if (store.iconOverride && store.iconHover) {
|
|
||||||
clearIcon()
|
|
||||||
} else {
|
|
||||||
iconInput?.click()
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
|
onDrop={model.drop}
|
||||||
|
onDragOver={model.dragOver}
|
||||||
|
onDragLeave={model.dragLeave}
|
||||||
|
onClick={model.iconClick}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={getProjectAvatarSource(props.project.id, {
|
when={getProjectAvatarSource(props.project.id, {
|
||||||
color: store.color,
|
color: model.store.color,
|
||||||
url: props.project.icon?.url,
|
url: props.project.icon?.url,
|
||||||
override: store.iconOverride,
|
override: model.store.iconOverride,
|
||||||
})}
|
})}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="size-full flex items-center justify-center">
|
<div class="size-full flex items-center justify-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
fallback={store.name || defaultName()}
|
fallback={model.store.name || model.defaultName()}
|
||||||
{...getAvatarColors(store.color)}
|
{...getAvatarColors(model.store.color)}
|
||||||
class="size-full text-[32px]"
|
class="size-full text-[32px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,8 +77,8 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
|
|||||||
<div
|
<div
|
||||||
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100": store.iconHover && !store.iconOverride,
|
"opacity-100": model.store.iconHover && !model.store.iconOverride,
|
||||||
"opacity-0": !(store.iconHover && !store.iconOverride),
|
"opacity-0": !(model.store.iconHover && !model.store.iconOverride),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||||
@@ -183,8 +86,8 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
|
|||||||
<div
|
<div
|
||||||
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100": store.iconHover && !!store.iconOverride,
|
"opacity-100": model.store.iconHover && !!model.store.iconOverride,
|
||||||
"opacity-0": !(store.iconHover && !!store.iconOverride),
|
"opacity-0": !(model.store.iconHover && !!model.store.iconOverride),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||||
@@ -193,12 +96,12 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
|
|||||||
<input
|
<input
|
||||||
id="icon-upload"
|
id="icon-upload"
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
iconInput = el
|
model.setIconInput(el)
|
||||||
}}
|
}}
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
onChange={handleInputChange}
|
onChange={model.inputChange}
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center">
|
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center">
|
||||||
<span>{language.t("dialog.project.edit.icon.hint")}</span>
|
<span>{language.t("dialog.project.edit.icon.hint")}</span>
|
||||||
@@ -207,7 +110,7 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!store.iconOverride}>
|
<Show when={!model.store.iconOverride}>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
|
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
|
||||||
<div class="flex gap-1.5">
|
<div class="flex gap-1.5">
|
||||||
@@ -216,21 +119,21 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={language.t("dialog.project.edit.color.select", { color })}
|
aria-label={language.t("dialog.project.edit.color.select", { color })}
|
||||||
aria-pressed={store.color === color}
|
aria-pressed={model.store.color === color}
|
||||||
classList={{
|
classList={{
|
||||||
"flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
"flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
||||||
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
|
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
|
||||||
store.color === color,
|
model.store.color === color,
|
||||||
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
||||||
store.color !== color,
|
model.store.color !== color,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (store.color === color && !props.project.icon?.url) return
|
if (model.store.color === color && !props.project.icon?.url) return
|
||||||
setStore("color", store.color === color ? undefined : color)
|
model.setStore("color", model.store.color === color ? undefined : color)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
fallback={store.name || defaultName()}
|
fallback={model.store.name || model.defaultName()}
|
||||||
{...getAvatarColors(color)}
|
{...getAvatarColors(color)}
|
||||||
class="size-full rounded"
|
class="size-full rounded"
|
||||||
/>
|
/>
|
||||||
@@ -246,19 +149,19 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
|
|||||||
label={language.t("dialog.project.edit.worktree.startup")}
|
label={language.t("dialog.project.edit.worktree.startup")}
|
||||||
description={language.t("dialog.project.edit.worktree.startup.description")}
|
description={language.t("dialog.project.edit.worktree.startup.description")}
|
||||||
placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
|
placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
|
||||||
value={store.startup}
|
value={model.store.startup}
|
||||||
onChange={(v) => setStore("startup", v)}
|
onChange={(v) => model.setStore("startup", v)}
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
class="max-h-14 w-full overflow-y-auto font-mono text-xs"
|
class="max-h-14 w-full overflow-y-auto font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
|
<Button type="button" variant="ghost" size="large" onClick={model.close}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" variant="primary" size="large" disabled={saveMutation.isPending}>
|
<Button type="submit" variant="primary" size="large" disabled={model.save.isPending}>
|
||||||
{saveMutation.isPending ? language.t("common.saving") : language.t("common.save")}
|
{model.save.isPending ? language.t("common.saving") : language.t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
|
import { createMemo } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { useGlobal } from "@/context/global"
|
||||||
|
import { type LocalProject } from "@/context/layout"
|
||||||
|
import { ServerConnection } from "@/context/server"
|
||||||
|
|
||||||
|
export function createEditProjectModel(props: { project: LocalProject; server: ServerConnection.Any }) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const global = useGlobal()
|
||||||
|
const serverCtx = createMemo(() => global.ensureServerCtx(props.server))
|
||||||
|
const folderName = createMemo(() => getFilename(props.project.worktree))
|
||||||
|
const defaultName = createMemo(() => props.project.name || folderName())
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
name: defaultName(),
|
||||||
|
color: props.project.icon?.color,
|
||||||
|
iconOverride: props.project.icon?.override,
|
||||||
|
startup: props.project.commands?.start ?? "",
|
||||||
|
dragOver: false,
|
||||||
|
iconHover: false,
|
||||||
|
})
|
||||||
|
let iconInput: HTMLInputElement | undefined
|
||||||
|
|
||||||
|
function selectFile(file: File) {
|
||||||
|
if (!file.type.startsWith("image/")) return
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const result = event.target?.result
|
||||||
|
if (typeof result !== "string") return
|
||||||
|
setStore("iconOverride", result)
|
||||||
|
setStore("iconHover", false)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
function drop(event: DragEvent) {
|
||||||
|
event.preventDefault()
|
||||||
|
setStore("dragOver", false)
|
||||||
|
const file = event.dataTransfer?.files[0]
|
||||||
|
if (file) selectFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragOver(event: DragEvent) {
|
||||||
|
event.preventDefault()
|
||||||
|
setStore("dragOver", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragLeave() {
|
||||||
|
setStore("dragOver", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputChange(event: Event) {
|
||||||
|
const file = (event.currentTarget as HTMLInputElement).files?.[0]
|
||||||
|
if (file) selectFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconClick() {
|
||||||
|
if (store.iconOverride && store.iconHover) {
|
||||||
|
setStore("iconOverride", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
iconInput?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = useMutation(() => ({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
||||||
|
const start = store.startup.trim()
|
||||||
|
|
||||||
|
if (props.project.id && props.project.id !== "global") {
|
||||||
|
await serverCtx().sdk.client.project.update({
|
||||||
|
projectID: props.project.id,
|
||||||
|
directory: props.project.worktree,
|
||||||
|
name,
|
||||||
|
icon: { color: store.color || "", override: store.iconOverride || "" },
|
||||||
|
commands: { start },
|
||||||
|
})
|
||||||
|
serverCtx().sync.project.icon(props.project.worktree, store.iconOverride || undefined)
|
||||||
|
dialog.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCtx().sync.project.meta(props.project.worktree, {
|
||||||
|
name,
|
||||||
|
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
|
||||||
|
commands: { start: start || undefined },
|
||||||
|
})
|
||||||
|
dialog.close()
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
function submit(event: SubmitEvent) {
|
||||||
|
event.preventDefault()
|
||||||
|
if (save.isPending) return
|
||||||
|
save.mutate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
setStore,
|
||||||
|
folderName,
|
||||||
|
defaultName,
|
||||||
|
save,
|
||||||
|
submit,
|
||||||
|
drop,
|
||||||
|
dragOver,
|
||||||
|
dragLeave,
|
||||||
|
inputChange,
|
||||||
|
iconClick,
|
||||||
|
close() {
|
||||||
|
dialog.close()
|
||||||
|
},
|
||||||
|
setIconInput(input: HTMLInputElement) {
|
||||||
|
iconInput = input
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { buildFileTreeV2Model, flattenFileTreeV2 } from "./file-tree-v2-model"
|
import { buildFileTreeV2Model, flattenFileTreeV2, flattenLiveFileTreeV2 } from "./file-tree-v2-model"
|
||||||
|
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
describe("file tree v2 model", () => {
|
describe("buildFileTreeV2Model", () => {
|
||||||
test("builds sorted depth-first rows", () => {
|
test("builds a sorted tree and flattens expanded directories", () => {
|
||||||
const model = buildFileTreeV2Model(["src/z.ts", "src/lib/b.ts", "src/lib/a.ts", "README.md", "docs/guide.md"])
|
const model = buildFileTreeV2Model(["src/z.ts", "src/lib/b.ts", "src/lib/a.ts", "README.md", "docs/guide.md"])
|
||||||
|
|
||||||
expect(model.total).toBe(8)
|
expect(model.total).toBe(8)
|
||||||
@@ -18,7 +19,7 @@ describe("file tree v2 model", () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("omits descendants of collapsed directories", () => {
|
test("skips children of collapsed directories", () => {
|
||||||
const model = buildFileTreeV2Model(["src/lib/a.ts", "src/z.ts"])
|
const model = buildFileTreeV2Model(["src/lib/a.ts", "src/z.ts"])
|
||||||
|
|
||||||
expect(flattenFileTreeV2(model, (path) => path !== "src/lib").map((row) => row.node.path)).toEqual([
|
expect(flattenFileTreeV2(model, (path) => path !== "src/lib").map((row) => row.node.path)).toEqual([
|
||||||
@@ -28,19 +29,46 @@ describe("file tree v2 model", () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("normalizes separators and duplicate paths", () => {
|
test("normalizes duplicate and messy paths", () => {
|
||||||
const model = buildFileTreeV2Model(["src\\lib\\a.ts", "src/lib/a.ts", "/src//lib/b.ts/"])
|
const model = buildFileTreeV2Model(["src\\lib\\a.ts", "src/lib/a.ts", "/src//lib/b.ts/"])
|
||||||
const rows = flattenFileTreeV2(model, () => true)
|
const rows = flattenFileTreeV2(model, () => true)
|
||||||
|
|
||||||
expect(model.total).toBe(4)
|
|
||||||
expect(rows.map((row) => row.node.path)).toEqual(["src", "src/lib", "src/lib/a.ts", "src/lib/b.ts"])
|
expect(rows.map((row) => row.node.path)).toEqual(["src", "src/lib", "src/lib/a.ts", "src/lib/b.ts"])
|
||||||
expect(rows.find((row) => row.node.path === "src/lib/a.ts")?.node.originalPath).toBe("src\\lib\\a.ts")
|
expect(rows.find((row) => row.node.path === "src/lib/a.ts")?.node.originalPath).toBe("src\\lib\\a.ts")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("supports paths deeper than the legacy recursion limit", () => {
|
test("handles deeply nested paths", () => {
|
||||||
const file = `${Array.from({ length: 130 }, (_, index) => `dir-${index}`).join("/")}/file.ts`
|
const file = Array.from({ length: 130 }, (_, index) => `d${index}`).join("/") + "/leaf.ts"
|
||||||
const model = buildFileTreeV2Model([file])
|
const model = buildFileTreeV2Model([file])
|
||||||
|
|
||||||
expect(flattenFileTreeV2(model, () => true)).toHaveLength(131)
|
expect(flattenFileTreeV2(model, () => true)).toHaveLength(131)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("flattenLiveFileTreeV2", () => {
|
||||||
|
test("flattens live children using original paths for nested lookups", () => {
|
||||||
|
const nodes: Record<string, FileNode[]> = {
|
||||||
|
"": [
|
||||||
|
{ name: "src", path: "src", absolute: "/repo/src", type: "directory", ignored: false },
|
||||||
|
{ name: "README.md", path: "README.md", absolute: "/repo/README.md", type: "file", ignored: false },
|
||||||
|
],
|
||||||
|
src: [
|
||||||
|
{ name: "a.ts", path: "src/a.ts", absolute: "/repo/src/a.ts", type: "file", ignored: false },
|
||||||
|
{ name: "lib", path: "src/lib", absolute: "/repo/src/lib", type: "directory", ignored: false },
|
||||||
|
],
|
||||||
|
"src/lib": [{ name: "b.ts", path: "src/lib/b.ts", absolute: "/repo/src/lib/b.ts", type: "file", ignored: false }],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
flattenLiveFileTreeV2(
|
||||||
|
(path) => nodes[path] ?? [],
|
||||||
|
(path) => path === "src",
|
||||||
|
).map((row) => [row.node.path, row.node.originalPath, row.level]),
|
||||||
|
).toEqual([
|
||||||
|
["src", "src", 0],
|
||||||
|
["src/a.ts", "src/a.ts", 1],
|
||||||
|
["src/lib", "src/lib", 1],
|
||||||
|
["README.md", "README.md", 0],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -75,3 +75,33 @@ export function flattenFileTreeV2(model: FileTreeV2Model, expanded: (path: strin
|
|||||||
|
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function flattenLiveFileTreeV2(
|
||||||
|
children: (path: string) => readonly FileNode[],
|
||||||
|
expanded: (path: string) => boolean,
|
||||||
|
) {
|
||||||
|
const rows: FileTreeV2Row[] = []
|
||||||
|
const stack = children("")
|
||||||
|
.toReversed()
|
||||||
|
.map((node) => ({ node: toLiveNode(node), level: 0 }))
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const row = stack.pop()!
|
||||||
|
rows.push(row)
|
||||||
|
if (row.node.type !== "directory" || !expanded(row.node.path)) continue
|
||||||
|
const nested = children(row.node.originalPath)
|
||||||
|
for (let index = nested.length - 1; index >= 0; index--) {
|
||||||
|
stack.push({ node: toLiveNode(nested[index]!), level: row.level + 1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLiveNode(node: FileNode): FileTreeV2Node {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
path: normalizeFileTreeV2Path(node.path),
|
||||||
|
originalPath: node.path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,13 @@ import type { FileNode } from "@opencode-ai/sdk/v2"
|
|||||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { pathToFileUrl, withFileDragImage, type Kind } from "@/components/file-tree"
|
import { pathToFileUrl, withFileDragImage, type Kind } from "@/components/file-tree"
|
||||||
import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
|
import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
|
||||||
import { buildFileTreeV2Model, flattenFileTreeV2, normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
|
import {
|
||||||
|
buildFileTreeV2Model,
|
||||||
|
flattenFileTreeV2,
|
||||||
|
flattenLiveFileTreeV2,
|
||||||
|
normalizeFileTreeV2Path,
|
||||||
|
type FileTreeV2Node,
|
||||||
|
} from "@/components/file-tree-v2-model"
|
||||||
import { virtualScrollElement } from "@/components/virtual-scroll-element"
|
import { virtualScrollElement } from "@/components/virtual-scroll-element"
|
||||||
|
|
||||||
export type { Kind } from "@/components/file-tree"
|
export type { Kind } from "@/components/file-tree"
|
||||||
@@ -36,7 +42,7 @@ function guideLineLeft(level: number) {
|
|||||||
export const kindLabel = (kind: Kind) => {
|
export const kindLabel = (kind: Kind) => {
|
||||||
if (kind === "add") return "A"
|
if (kind === "add") return "A"
|
||||||
if (kind === "del") return "D"
|
if (kind === "del") return "D"
|
||||||
return ""
|
return "M"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const kindChange = (kind: Kind) => {
|
export const kindChange = (kind: Kind) => {
|
||||||
@@ -68,7 +74,7 @@ const FileTreeNodeV2 = (
|
|||||||
"class",
|
"class",
|
||||||
"classList",
|
"classList",
|
||||||
])
|
])
|
||||||
const kind = () => local.kinds?.get(local.node.path)
|
const kind = () => local.kinds?.get(normalizeFileTreeV2Path(local.node.path))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dynamic
|
<Dynamic
|
||||||
@@ -110,12 +116,7 @@ const FileTreeNodeV2 = (
|
|||||||
function GuideLines(props: { level: number }) {
|
function GuideLines(props: { level: number }) {
|
||||||
return (
|
return (
|
||||||
<For each={Array.from({ length: props.level })}>
|
<For each={Array.from({ length: props.level })}>
|
||||||
{(_, index) => (
|
{(_, index) => <div data-slot="file-tree-v2-guide" style={`left: ${guideLineLeft(index())}px`} />}
|
||||||
<div
|
|
||||||
class="absolute top-0 bottom-0 w-px pointer-events-none bg-border-weak-base opacity-0 group-hover/file-tree-v2:opacity-50"
|
|
||||||
style={`left: ${guideLineLeft(index())}px`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
</For>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -126,12 +127,18 @@ export default function FileTreeV2(props: {
|
|||||||
kinds?: ReadonlyMap<string, Kind>
|
kinds?: ReadonlyMap<string, Kind>
|
||||||
draggable?: boolean
|
draggable?: boolean
|
||||||
onFileClick?: (file: FileNode) => void
|
onFileClick?: (file: FileNode) => void
|
||||||
|
onFileDoubleClick?: (file: FileNode) => void
|
||||||
}) {
|
}) {
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
|
const live = () => props.allowed === undefined
|
||||||
const draggable = () => props.draggable ?? true
|
const draggable = () => props.draggable ?? true
|
||||||
const active = () => normalizeFileTreeV2Path(props.active ?? "")
|
const active = () => normalizeFileTreeV2Path(props.active ?? "")
|
||||||
const model = createMemo(() => buildFileTreeV2Model(props.allowed ?? []))
|
const model = createMemo(() => (live() ? undefined : buildFileTreeV2Model(props.allowed ?? [])))
|
||||||
const rows = createMemo(() => flattenFileTreeV2(model(), (path) => file.tree.state(path)?.expanded ?? true))
|
const expanded = (path: string) => file.tree.state(path)?.expanded ?? !live()
|
||||||
|
const rows = createMemo(() => {
|
||||||
|
if (live()) return flattenLiveFileTreeV2((path) => file.tree.children(path), expanded)
|
||||||
|
return flattenFileTreeV2(model()!, expanded)
|
||||||
|
})
|
||||||
const [root, setRoot] = createSignal<HTMLDivElement>()
|
const [root, setRoot] = createSignal<HTMLDivElement>()
|
||||||
const [focused, setFocused] = createSignal<string>()
|
const [focused, setFocused] = createSignal<string>()
|
||||||
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
|
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
|
||||||
@@ -155,16 +162,49 @@ export default function FileTreeV2(props: {
|
|||||||
return [...indexes, index].sort((a, b) => a - b)
|
return [...indexes, index].sort((a, b) => a - b)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!live()) return
|
||||||
|
void file.tree.list("")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Only scroll when the active path changes (or first appears in the tree).
|
||||||
|
// Do not re-scroll when expand/collapse reshuffles `rows()`.
|
||||||
|
let scrolledActive: string | undefined
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const path = active()
|
const path = active()
|
||||||
if (!path) return
|
if (!path) {
|
||||||
|
scrolledActive = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
const index = rows().findIndex((row) => row.node.path === path)
|
const index = rows().findIndex((row) => row.node.path === path)
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
|
if (scrolledActive === path) return
|
||||||
|
scrolledActive = path
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
if (virtualizer.range && index >= virtualizer.range.startIndex && index <= virtualizer.range.endIndex) return
|
const next = rows().findIndex((row) => row.node.path === path)
|
||||||
virtualizer.scrollToIndex(index, { align: "auto" })
|
if (next < 0) return
|
||||||
|
if (virtualizer.range && next >= virtualizer.range.startIndex && next <= virtualizer.range.endIndex) return
|
||||||
|
virtualizer.scrollToIndex(next, { align: "auto" })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectFile = (node: FileTreeV2Node, action?: (file: FileNode) => void) => {
|
||||||
|
action?.({
|
||||||
|
...node,
|
||||||
|
path: node.originalPath,
|
||||||
|
absolute: node.originalPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleDirectory = (path: string, originalPath: string) => {
|
||||||
|
if (expanded(path)) {
|
||||||
|
file.tree.collapse(originalPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file.tree.expand(originalPath, live() ? undefined : { list: false })
|
||||||
|
}
|
||||||
|
|
||||||
const rowByKey = createMemo(() => new Map(rows().map((row) => [row.node.path, row] as const)))
|
const rowByKey = createMemo(() => new Map(rows().map((row) => [row.node.path, row] as const)))
|
||||||
const virtualItemByKey = createMemo(
|
const virtualItemByKey = createMemo(
|
||||||
() => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)),
|
() => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)),
|
||||||
@@ -175,7 +215,7 @@ export default function FileTreeV2(props: {
|
|||||||
<div
|
<div
|
||||||
ref={setRoot}
|
ref={setRoot}
|
||||||
data-component="file-tree-v2"
|
data-component="file-tree-v2"
|
||||||
data-total-rows={model().total}
|
data-total-rows={live() ? rows().length : model()!.total}
|
||||||
class="group/file-tree-v2"
|
class="group/file-tree-v2"
|
||||||
style={{ position: "relative", height: `${virtualizer.getTotalSize()}px` }}
|
style={{ position: "relative", height: `${virtualizer.getTotalSize()}px` }}
|
||||||
>
|
>
|
||||||
@@ -209,13 +249,8 @@ export default function FileTreeV2(props: {
|
|||||||
class="relative"
|
class="relative"
|
||||||
onFocus={() => setFocused(row().node.path)}
|
onFocus={() => setFocused(row().node.path)}
|
||||||
onBlur={() => setFocused(undefined)}
|
onBlur={() => setFocused(undefined)}
|
||||||
onClick={() =>
|
onClick={() => selectFile(row().node, props.onFileClick)}
|
||||||
props.onFileClick?.({
|
onDblClick={() => selectFile(row().node, props.onFileDoubleClick)}
|
||||||
...row().node,
|
|
||||||
path: row().node.originalPath,
|
|
||||||
absolute: row().node.originalPath,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<GuideLines level={row().level} />
|
<GuideLines level={row().level} />
|
||||||
<Show when={row().level > 0}>
|
<Show when={row().level > 0}>
|
||||||
@@ -239,17 +274,13 @@ export default function FileTreeV2(props: {
|
|||||||
class="relative"
|
class="relative"
|
||||||
onFocus={() => setFocused(row().node.path)}
|
onFocus={() => setFocused(row().node.path)}
|
||||||
onBlur={() => setFocused(undefined)}
|
onBlur={() => setFocused(undefined)}
|
||||||
aria-expanded={file.tree.state(row().node.path)?.expanded ?? true}
|
aria-expanded={expanded(row().node.path)}
|
||||||
onClick={() =>
|
onClick={() => toggleDirectory(row().node.path, row().node.originalPath)}
|
||||||
file.tree.state(row().node.path)?.expanded === false
|
|
||||||
? file.tree.expand(row().node.path, { list: false })
|
|
||||||
: file.tree.collapse(row().node.path)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<GuideLines level={row().level} />
|
<GuideLines level={row().level} />
|
||||||
<div
|
<div
|
||||||
data-slot="file-tree-v2-chevron"
|
data-slot="file-tree-v2-chevron"
|
||||||
data-expanded={file.tree.state(row().node.path)?.expanded === false ? undefined : ""}
|
data-expanded={expanded(row().node.path) ? "" : undefined}
|
||||||
class="size-4 flex items-center justify-center"
|
class="size-4 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<Icon name="chevron-down" />
|
<Icon name="chevron-down" />
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/f
|
|||||||
import {
|
import {
|
||||||
ContentPart,
|
ContentPart,
|
||||||
DEFAULT_PROMPT,
|
DEFAULT_PROMPT,
|
||||||
|
isCommentItem,
|
||||||
isPromptEqual,
|
isPromptEqual,
|
||||||
Prompt,
|
Prompt,
|
||||||
usePrompt,
|
usePrompt,
|
||||||
@@ -1626,7 +1627,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<PromptContextItems
|
<PromptContextItems
|
||||||
items={contextItems()}
|
items={contextItems().filter((item) => !isCommentItem(item))}
|
||||||
active={(item) => {
|
active={(item) => {
|
||||||
const active = comments.active()
|
const active = comments.active()
|
||||||
return !!item.commentID && item.commentID === active?.id && item.path === active?.file
|
return !!item.commentID && item.commentID === active?.id && item.path === active?.file
|
||||||
@@ -1636,6 +1637,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
if (item.commentID) comments.remove(item.path, item.commentID)
|
if (item.commentID) comments.remove(item.path, item.commentID)
|
||||||
prompt.context.remove(item.key)
|
prompt.context.remove(item.key)
|
||||||
}}
|
}}
|
||||||
|
newLayoutDesigns={props.controls.newLayoutDesigns}
|
||||||
t={(key) => language.t(key as Parameters<typeof language.t>[0])}
|
t={(key) => language.t(key as Parameters<typeof language.t>[0])}
|
||||||
/>
|
/>
|
||||||
<PromptImageAttachments
|
<PromptImageAttachments
|
||||||
@@ -1645,6 +1647,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
onRemove={removeAttachment}
|
onRemove={removeAttachment}
|
||||||
removeLabel={language.t("prompt.attachment.remove")}
|
removeLabel={language.t("prompt.attachment.remove")}
|
||||||
|
newLayoutDesigns={props.controls.newLayoutDesigns}
|
||||||
|
comments={contextItems().filter(isCommentItem)}
|
||||||
|
commentActive={(item) => {
|
||||||
|
const active = comments.active()
|
||||||
|
return !!item.commentID && item.commentID === active?.id && item.path === active?.file
|
||||||
|
}}
|
||||||
|
onOpenComment={openComment}
|
||||||
|
onRemoveComment={(item) => {
|
||||||
|
if (item.commentID) comments.remove(item.path, item.commentID)
|
||||||
|
prompt.context.remove(item.key)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="relative min-h-[52px]"
|
class="relative min-h-[52px]"
|
||||||
@@ -1852,6 +1865,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
if (item.commentID) comments.remove(item.path, item.commentID)
|
if (item.commentID) comments.remove(item.path, item.commentID)
|
||||||
prompt.context.remove(item.key)
|
prompt.context.remove(item.key)
|
||||||
}}
|
}}
|
||||||
|
newLayoutDesigns={props.controls.newLayoutDesigns}
|
||||||
t={(key) => language.t(key as Parameters<typeof language.t>[0])}
|
t={(key) => language.t(key as Parameters<typeof language.t>[0])}
|
||||||
/>
|
/>
|
||||||
<PromptImageAttachments
|
<PromptImageAttachments
|
||||||
@@ -1861,6 +1875,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
onRemove={removeAttachment}
|
onRemove={removeAttachment}
|
||||||
removeLabel={language.t("prompt.attachment.remove")}
|
removeLabel={language.t("prompt.attachment.remove")}
|
||||||
|
newLayoutDesigns={props.controls.newLayoutDesigns}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="relative"
|
class="relative"
|
||||||
@@ -2341,7 +2356,7 @@ function ModelControlContent(props: { state: ComposerModelControlState; v2?: boo
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<span class="truncate">{props.state.modelName}</span>
|
<span class="truncate leading-4">{props.state.modelName}</span>
|
||||||
<span class={props.v2 ? "-ml-0.5 -mr-1 flex shrink-0" : "-ml-1 shrink-0 flex size-fit"}>
|
<span class={props.v2 ? "-ml-0.5 -mr-1 flex shrink-0" : "-ml-1 shrink-0 flex size-fit"}>
|
||||||
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
|
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Component, For, Show } from "solid-js"
|
import { Component, For, Show } from "solid-js"
|
||||||
|
import { Dynamic } from "solid-js/web"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/core/util/path"
|
||||||
import type { ContextItem } from "@/context/prompt"
|
import type { ContextItem } from "@/context/prompt"
|
||||||
@@ -12,6 +14,7 @@ type ContextItemsProps = {
|
|||||||
active: (item: PromptContextItem) => boolean
|
active: (item: PromptContextItem) => boolean
|
||||||
openComment: (item: PromptContextItem) => void
|
openComment: (item: PromptContextItem) => void
|
||||||
remove: (item: PromptContextItem) => void
|
remove: (item: PromptContextItem) => void
|
||||||
|
newLayoutDesigns: boolean
|
||||||
t: (key: string) => string
|
t: (key: string) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,10 +30,17 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
|
|||||||
const selected = props.active(item)
|
const selected = props.active(item)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipV2
|
<Dynamic
|
||||||
|
component={props.newLayoutDesigns ? TooltipV2 : Tooltip}
|
||||||
value={
|
value={
|
||||||
<span class="flex max-w-[300px]">
|
<span class="flex max-w-[300px]">
|
||||||
<span class="text-text-invert-base truncate-start [unicode-bidi:plaintext] min-w-0">
|
<span
|
||||||
|
classList={{
|
||||||
|
"truncate-start [unicode-bidi:plaintext] min-w-0": true,
|
||||||
|
"text-v2-text-text-muted": props.newLayoutDesigns,
|
||||||
|
"text-text-invert-base": !props.newLayoutDesigns,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{directory}
|
{directory}
|
||||||
</span>
|
</span>
|
||||||
<span class="shrink-0">{filename}</span>
|
<span class="shrink-0">{filename}</span>
|
||||||
@@ -78,7 +88,7 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
|
|||||||
{(comment) => <div class="text-12-regular text-text-strong ml-5 pr-1 truncate">{comment()}</div>}
|
{(comment) => <div class="text-12-regular text-text-strong ml-5 pr-1 truncate">{comment()}</div>}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</TooltipV2>
|
</Dynamic>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
@keyframes prompt-attachments-fade-left {
|
||||||
|
from {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes prompt-attachments-fade-right {
|
||||||
|
from {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="prompt-attachments"] {
|
||||||
|
timeline-scope: --prompt-attachments-scroll;
|
||||||
|
|
||||||
|
[data-slot^="prompt-attachments-fade-"] {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (animation-timeline: --prompt-attachments-scroll) and (timeline-scope: --prompt-attachments-scroll) {
|
||||||
|
[data-slot="prompt-attachments-scroll"] {
|
||||||
|
scroll-timeline: --prompt-attachments-scroll x;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="prompt-attachments-fade-left"] {
|
||||||
|
animation: prompt-attachments-fade-left linear both;
|
||||||
|
animation-timeline: --prompt-attachments-scroll;
|
||||||
|
animation-range: 0 0.1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="prompt-attachments-fade-right"] {
|
||||||
|
animation: prompt-attachments-fade-right linear both;
|
||||||
|
animation-timeline: --prompt-attachments-scroll;
|
||||||
|
animation-range: calc(100% - 1.1px) calc(100% - 1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +1,168 @@
|
|||||||
import { Component, For, Show } from "solid-js"
|
import { Component, For, Show } from "solid-js"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import type { ImageAttachmentPart } from "@/context/prompt"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
|
import { AttachmentCardV2 } from "@opencode-ai/session-ui/v2/attachment-card-v2"
|
||||||
|
import { CommentCardV2 } from "@opencode-ai/session-ui/v2/comment-card-v2"
|
||||||
|
import { typeLabel } from "@opencode-ai/session-ui/message-file"
|
||||||
|
import type { ContextItem, ImageAttachmentPart } from "@/context/prompt"
|
||||||
|
import "./image-attachments.css"
|
||||||
|
|
||||||
|
type PromptCommentItem = ContextItem & { key: string }
|
||||||
|
|
||||||
type PromptImageAttachmentsProps = {
|
type PromptImageAttachmentsProps = {
|
||||||
attachments: ImageAttachmentPart[]
|
attachments: ImageAttachmentPart[]
|
||||||
onOpen: (attachment: ImageAttachmentPart) => void
|
onOpen: (attachment: ImageAttachmentPart) => void
|
||||||
onRemove: (id: string) => void
|
onRemove: (id: string) => void
|
||||||
removeLabel: string
|
removeLabel: string
|
||||||
|
newLayoutDesigns: boolean
|
||||||
|
comments?: PromptCommentItem[]
|
||||||
|
commentActive?: (item: PromptCommentItem) => boolean
|
||||||
|
onOpenComment?: (item: PromptCommentItem) => void
|
||||||
|
onRemoveComment?: (item: PromptCommentItem) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackClass = "size-16 rounded-md bg-surface-base flex items-center justify-center border border-border-base"
|
const fallbackClass = "size-16 rounded-md bg-surface-base flex items-center justify-center border border-border-base"
|
||||||
const imageClass =
|
const imageClass =
|
||||||
"size-16 rounded-md object-cover border border-border-base hover:border-border-strong-base transition-colors"
|
"size-16 rounded-md object-cover border border-border-base hover:border-border-strong-base transition-colors"
|
||||||
|
const imageClassV2 = "w-[58px] h-[46px] rounded-[6px] object-cover"
|
||||||
|
// inset box-shadows do not paint over <img> content, so the hairline is a separate overlay
|
||||||
|
const imageHairlineClassV2 =
|
||||||
|
"absolute inset-0 rounded-[6px] shadow-[inset_0_0_0_0.5px_var(--v2-border-border-base)] pointer-events-none"
|
||||||
const removeClass =
|
const removeClass =
|
||||||
"absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover"
|
"absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover"
|
||||||
|
const removeClassV2 =
|
||||||
|
"absolute -top-1 -right-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
const nameClass = "absolute bottom-0 left-0 right-0 px-1 py-0.5 bg-black/50 rounded-b-md"
|
const nameClass = "absolute bottom-0 left-0 right-0 px-1 py-0.5 bg-black/50 rounded-b-md"
|
||||||
|
|
||||||
export const PromptImageAttachments: Component<PromptImageAttachmentsProps> = (props) => {
|
export const PromptImageAttachments: Component<PromptImageAttachmentsProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<Show when={props.attachments.length > 0}>
|
<Show when={props.attachments.length > 0 || (props.newLayoutDesigns && (props.comments?.length ?? 0) > 0)}>
|
||||||
<div class="flex flex-wrap gap-2 px-3 pt-3">
|
<div data-slot="prompt-attachments" classList={{ relative: props.newLayoutDesigns }}>
|
||||||
|
<div
|
||||||
|
data-slot="prompt-attachments-scroll"
|
||||||
|
classList={{
|
||||||
|
"flex gap-2": true,
|
||||||
|
"flex-nowrap overflow-x-auto no-scrollbar px-2 pt-2 pb-1": props.newLayoutDesigns,
|
||||||
|
"flex-wrap px-3 pt-3": !props.newLayoutDesigns,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={props.newLayoutDesigns}>
|
||||||
|
<For each={props.comments ?? []}>
|
||||||
|
{(item) => (
|
||||||
|
<div class="relative group shrink-0">
|
||||||
|
<TooltipV2
|
||||||
|
value={item.comment}
|
||||||
|
placement="top"
|
||||||
|
openDelay={800}
|
||||||
|
contentClass="max-w-[300px] break-words"
|
||||||
|
>
|
||||||
|
<CommentCardV2
|
||||||
|
comment={item.comment ?? ""}
|
||||||
|
path={item.path}
|
||||||
|
selection={item.selection}
|
||||||
|
active={props.commentActive?.(item)}
|
||||||
|
onClick={() => props.onOpenComment?.(item)}
|
||||||
|
/>
|
||||||
|
</TooltipV2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => props.onRemoveComment?.(item)}
|
||||||
|
class={removeClassV2}
|
||||||
|
aria-label={props.removeLabel}
|
||||||
|
>
|
||||||
|
<IconV2 name="outline-xmark" class="text-v2-icon-icon-contrast" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
<For each={props.attachments}>
|
<For each={props.attachments}>
|
||||||
{(attachment) => (
|
{(attachment) => {
|
||||||
<Tooltip value={attachment.filename} placement="top" contentClass="break-all">
|
const image = attachment.mime.startsWith("image/")
|
||||||
<div class="relative group">
|
const media = () => (
|
||||||
<Show
|
<Show
|
||||||
when={attachment.mime.startsWith("image/")}
|
when={image}
|
||||||
|
fallback={
|
||||||
|
<Show
|
||||||
|
when={props.newLayoutDesigns}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={fallbackClass}>
|
<div class={fallbackClass}>
|
||||||
<Icon name="folder" class="size-6 text-text-weak" />
|
<Icon name="folder" class="size-6 text-text-weak" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<AttachmentCardV2 title={attachment.filename}>
|
||||||
|
{typeLabel(attachment.filename, attachment.mime)}
|
||||||
|
</AttachmentCardV2>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={attachment.dataUrl}
|
src={attachment.dataUrl}
|
||||||
alt={attachment.filename}
|
alt={attachment.filename}
|
||||||
class={imageClass}
|
class={props.newLayoutDesigns ? imageClassV2 : imageClass}
|
||||||
onClick={() => props.onOpen(attachment)}
|
onClick={() => props.onOpen(attachment)}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<button
|
)
|
||||||
type="button"
|
const name = () => (
|
||||||
onClick={() => props.onRemove(attachment.id)}
|
|
||||||
class={removeClass}
|
|
||||||
aria-label={props.removeLabel}
|
|
||||||
>
|
|
||||||
<Icon name="close" class="size-3 text-text-weak" />
|
|
||||||
</button>
|
|
||||||
<div class={nameClass}>
|
<div class={nameClass}>
|
||||||
<span class="text-10-regular text-white truncate block">{attachment.filename}</span>
|
<span class="text-10-regular text-white truncate block">{attachment.filename}</span>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
const remove = () => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => props.onRemove(attachment.id)}
|
||||||
|
class={props.newLayoutDesigns ? removeClassV2 : removeClass}
|
||||||
|
aria-label={props.removeLabel}
|
||||||
|
>
|
||||||
|
<Show when={props.newLayoutDesigns} fallback={<Icon name="close" class="size-3 text-text-weak" />}>
|
||||||
|
<IconV2 name="outline-xmark" class="text-v2-icon-icon-contrast" />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
// v2 keeps the remove button outside the tooltip trigger so hovering it dismisses the tooltip
|
||||||
|
return (
|
||||||
|
<Show
|
||||||
|
when={props.newLayoutDesigns}
|
||||||
|
fallback={
|
||||||
|
<Tooltip value={attachment.filename} placement="top" contentClass="break-all">
|
||||||
|
<div class="relative group">
|
||||||
|
{media()}
|
||||||
|
{name()}
|
||||||
|
{remove()}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
}
|
||||||
|
>
|
||||||
|
<div class="relative group shrink-0">
|
||||||
|
<TooltipV2 value={attachment.filename} placement="top" contentClass="break-all">
|
||||||
|
{media()}
|
||||||
|
<Show when={image}>
|
||||||
|
<div class={imageHairlineClassV2} />
|
||||||
|
</Show>
|
||||||
|
</TooltipV2>
|
||||||
|
{remove()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={props.newLayoutDesigns}>
|
||||||
|
<div
|
||||||
|
data-slot="prompt-attachments-fade-left"
|
||||||
|
class="pointer-events-none absolute inset-y-0 left-0 z-10 w-6 bg-[linear-gradient(to_right,var(--v2-background-bg-base),transparent)]"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
data-slot="prompt-attachments-fade-right"
|
||||||
|
class="pointer-events-none absolute inset-y-0 right-0 z-10 w-6 bg-[linear-gradient(to_left,var(--v2-background-bg-base),transparent)]"
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { For, Show } from "solid-js"
|
import { For, Show } from "solid-js"
|
||||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
@@ -96,10 +97,17 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
{(branch) => (
|
{(branch) => (
|
||||||
<>
|
<>
|
||||||
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
|
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
|
||||||
|
<TooltipV2
|
||||||
|
placement="top"
|
||||||
|
value={branch()}
|
||||||
|
class="min-w-0 max-w-[220px]"
|
||||||
|
contentClass="max-w-[calc(100vw-32px)] break-all"
|
||||||
|
>
|
||||||
<div class="flex h-7 min-w-0 max-w-[220px] items-center gap-1.5 px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px]">
|
<div class="flex h-7 min-w-0 max-w-[220px] items-center gap-1.5 px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px]">
|
||||||
<Icon name="branch" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
<Icon name="branch" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
<span class="min-w-0 truncate">{branch()}</span>
|
<span class="min-w-0 truncate">{branch()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipV2>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export { SessionHeader } from "./session-header"
|
export { SessionHeader } from "./session-header"
|
||||||
export { SessionContextTab } from "./session-context-tab"
|
export { SessionContextTab } from "./session-context-tab"
|
||||||
export { SortableTab, FileVisual } from "./session-sortable-tab"
|
export { SortableTab, FileVisual } from "./session-sortable-tab"
|
||||||
|
export { SortableTabV2 } from "./session-sortable-tab-v2"
|
||||||
export { SortableTerminalTab } from "./session-sortable-terminal-tab"
|
export { SortableTerminalTab } from "./session-sortable-terminal-tab"
|
||||||
export { NewSessionView } from "./session-new-view"
|
export { NewSessionView } from "./session-new-view"
|
||||||
export { NewSessionDesignView } from "./session-new-design-view"
|
export { NewSessionDesignView } from "./session-new-design-view"
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { For, Show } from "solid-js"
|
||||||
|
import { AppIcon } from "@opencode-ai/ui/app-icon"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
|
import { SplitButtonV2, SplitButtonV2Action, SplitButtonV2MenuTrigger } from "@opencode-ai/ui/v2/split-button-v2"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { type OpenApp, useOpenInApp } from "@/components/session/open-in-app"
|
||||||
|
|
||||||
|
export function OpenInAppV2(props: { directory: () => string }) {
|
||||||
|
const language = useLanguage()
|
||||||
|
const state = useOpenInApp(props)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={props.directory() && state.canOpen()}>
|
||||||
|
<SplitButtonV2 class="session-review-v2-open-in-app" onPointerDown={(event) => event.stopPropagation()}>
|
||||||
|
<TooltipV2
|
||||||
|
placement="bottom"
|
||||||
|
value={language.t("session.header.open.ariaLabel", { app: state.current().label })}
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<SplitButtonV2Action
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
if (state.opening()) return
|
||||||
|
state.openDir(state.current().id)
|
||||||
|
}}
|
||||||
|
disabled={state.opening()}
|
||||||
|
aria-label={language.t("session.header.open.ariaLabel", { app: state.current().label })}
|
||||||
|
>
|
||||||
|
<Show when={state.opening()} fallback={<AppIcon id={state.current().icon} class="size-[18px]" />}>
|
||||||
|
<Spinner class="size-3.5" />
|
||||||
|
</Show>
|
||||||
|
</SplitButtonV2Action>
|
||||||
|
</TooltipV2>
|
||||||
|
<MenuV2
|
||||||
|
gutter={4}
|
||||||
|
modal={false}
|
||||||
|
placement="bottom-end"
|
||||||
|
open={state.menu.open}
|
||||||
|
onOpenChange={(open) => state.setMenu("open", open)}
|
||||||
|
>
|
||||||
|
<MenuV2.Trigger
|
||||||
|
as={SplitButtonV2MenuTrigger}
|
||||||
|
disabled={state.opening()}
|
||||||
|
aria-label={language.t("session.header.open.menu")}
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<IconV2 name="chevron-down" size="small" />
|
||||||
|
</MenuV2.Trigger>
|
||||||
|
<MenuV2.Portal>
|
||||||
|
<MenuV2.Content class="open-in-app-v2-menu">
|
||||||
|
<MenuV2.Group>
|
||||||
|
<MenuV2.GroupLabel>{language.t("session.header.openIn")}</MenuV2.GroupLabel>
|
||||||
|
<MenuV2.RadioGroup
|
||||||
|
value={state.current().id}
|
||||||
|
onChange={(value) => {
|
||||||
|
state.selectApp(value as OpenApp)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={state.options()}>
|
||||||
|
{(option) => (
|
||||||
|
<MenuV2.RadioItem
|
||||||
|
value={option.id}
|
||||||
|
disabled={state.opening()}
|
||||||
|
onSelect={() => {
|
||||||
|
state.selectApp(option.id)
|
||||||
|
state.setMenu("open", false)
|
||||||
|
state.openDir(option.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AppIcon id={option.icon} />
|
||||||
|
{option.label}
|
||||||
|
</MenuV2.RadioItem>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</MenuV2.RadioGroup>
|
||||||
|
</MenuV2.Group>
|
||||||
|
<MenuV2.Separator />
|
||||||
|
<MenuV2.Item
|
||||||
|
onSelect={() => {
|
||||||
|
state.setMenu("open", false)
|
||||||
|
state.copyPath()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="copy" size="small" class="text-icon-weak" />
|
||||||
|
{language.t("session.header.open.copyPath")}
|
||||||
|
</MenuV2.Item>
|
||||||
|
</MenuV2.Content>
|
||||||
|
</MenuV2.Portal>
|
||||||
|
</MenuV2>
|
||||||
|
</SplitButtonV2>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import { createEffect, createMemo } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useServer } from "@/context/server"
|
||||||
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
import { showToast } from "@/utils/toast"
|
||||||
|
|
||||||
|
export const OPEN_APPS = [
|
||||||
|
"vscode",
|
||||||
|
"cursor",
|
||||||
|
"zed",
|
||||||
|
"textmate",
|
||||||
|
"antigravity",
|
||||||
|
"finder",
|
||||||
|
"terminal",
|
||||||
|
"iterm2",
|
||||||
|
"ghostty",
|
||||||
|
"warp",
|
||||||
|
"xcode",
|
||||||
|
"android-studio",
|
||||||
|
"powershell",
|
||||||
|
"sublime-text",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type OpenApp = (typeof OPEN_APPS)[number]
|
||||||
|
export type OpenAppOS = "macos" | "windows" | "linux" | "unknown"
|
||||||
|
|
||||||
|
export const MAC_OPEN_APPS = [
|
||||||
|
{
|
||||||
|
id: "vscode",
|
||||||
|
label: "session.header.open.app.vscode",
|
||||||
|
icon: "vscode",
|
||||||
|
openWith: "Visual Studio Code",
|
||||||
|
},
|
||||||
|
{ id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "Cursor" },
|
||||||
|
{ id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "Zed" },
|
||||||
|
{ id: "textmate", label: "session.header.open.app.textmate", icon: "textmate", openWith: "TextMate" },
|
||||||
|
{
|
||||||
|
id: "antigravity",
|
||||||
|
label: "session.header.open.app.antigravity",
|
||||||
|
icon: "antigravity",
|
||||||
|
openWith: "Antigravity",
|
||||||
|
},
|
||||||
|
{ id: "terminal", label: "session.header.open.app.terminal", icon: "terminal", openWith: "Terminal" },
|
||||||
|
{ id: "iterm2", label: "session.header.open.app.iterm2", icon: "iterm2", openWith: "iTerm" },
|
||||||
|
{ id: "ghostty", label: "session.header.open.app.ghostty", icon: "ghostty", openWith: "Ghostty" },
|
||||||
|
{ id: "warp", label: "session.header.open.app.warp", icon: "warp", openWith: "Warp" },
|
||||||
|
{ id: "xcode", label: "session.header.open.app.xcode", icon: "xcode", openWith: "Xcode" },
|
||||||
|
{
|
||||||
|
id: "android-studio",
|
||||||
|
label: "session.header.open.app.androidStudio",
|
||||||
|
icon: "android-studio",
|
||||||
|
openWith: "Android Studio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sublime-text",
|
||||||
|
label: "session.header.open.app.sublimeText",
|
||||||
|
icon: "sublime-text",
|
||||||
|
openWith: "Sublime Text",
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const WINDOWS_OPEN_APPS = [
|
||||||
|
{ id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" },
|
||||||
|
{ id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" },
|
||||||
|
{ id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" },
|
||||||
|
{
|
||||||
|
id: "powershell",
|
||||||
|
label: "session.header.open.app.powershell",
|
||||||
|
icon: "powershell",
|
||||||
|
openWith: "powershell",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sublime-text",
|
||||||
|
label: "session.header.open.app.sublimeText",
|
||||||
|
icon: "sublime-text",
|
||||||
|
openWith: "Sublime Text",
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const LINUX_OPEN_APPS = [
|
||||||
|
{ id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" },
|
||||||
|
{ id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" },
|
||||||
|
{ id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" },
|
||||||
|
{
|
||||||
|
id: "sublime-text",
|
||||||
|
label: "session.header.open.app.sublimeText",
|
||||||
|
icon: "sublime-text",
|
||||||
|
openWith: "Sublime Text",
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export function detectOpenAppOS(platform: ReturnType<typeof usePlatform>): OpenAppOS {
|
||||||
|
if (platform.platform === "desktop" && platform.os) return platform.os
|
||||||
|
if (typeof navigator !== "object") return "unknown"
|
||||||
|
const value = navigator.platform || navigator.userAgent
|
||||||
|
if (/Mac/i.test(value)) return "macos"
|
||||||
|
if (/Win/i.test(value)) return "windows"
|
||||||
|
if (/Linux/i.test(value)) return "linux"
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openAppFileManager(os: OpenAppOS) {
|
||||||
|
if (os === "macos") return { label: "session.header.open.finder", icon: "finder" as const }
|
||||||
|
if (os === "windows") return { label: "session.header.open.fileExplorer", icon: "file-explorer" as const }
|
||||||
|
return { label: "session.header.open.fileManager", icon: "finder" as const }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openAppsForOS(os: OpenAppOS) {
|
||||||
|
if (os === "macos") return MAC_OPEN_APPS
|
||||||
|
if (os === "windows") return WINDOWS_OPEN_APPS
|
||||||
|
return LINUX_OPEN_APPS
|
||||||
|
}
|
||||||
|
|
||||||
|
const showRequestError = (language: ReturnType<typeof useLanguage>, err: unknown) => {
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: err instanceof Error ? err.message : String(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOpenInApp(input: { directory: () => string }) {
|
||||||
|
const platform = usePlatform()
|
||||||
|
const server = useServer()
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
|
const os = createMemo(() => detectOpenAppOS(platform))
|
||||||
|
const apps = createMemo(() => openAppsForOS(os()))
|
||||||
|
const fileManager = createMemo(() => openAppFileManager(os()))
|
||||||
|
|
||||||
|
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
||||||
|
finder: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (platform.platform !== "desktop") return
|
||||||
|
if (!platform.checkAppExists) return
|
||||||
|
|
||||||
|
const list = apps()
|
||||||
|
|
||||||
|
setExists(Object.fromEntries(list.map((app) => [app.id, undefined])) as Partial<Record<OpenApp, boolean>>)
|
||||||
|
|
||||||
|
void Promise.all(
|
||||||
|
list.map((app) =>
|
||||||
|
Promise.resolve(platform.checkAppExists?.(app.openWith))
|
||||||
|
.then((value) => Boolean(value))
|
||||||
|
.catch(() => false)
|
||||||
|
.then((ok) => [app.id, ok] as const),
|
||||||
|
),
|
||||||
|
).then((entries) => {
|
||||||
|
setExists(Object.fromEntries(entries) as Partial<Record<OpenApp, boolean>>)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = createMemo(() => {
|
||||||
|
return [
|
||||||
|
{ id: "finder", label: language.t(fileManager().label), icon: fileManager().icon },
|
||||||
|
...apps()
|
||||||
|
.filter((app) => exists[app.id])
|
||||||
|
.map((app) => ({ ...app, label: language.t(app.label) })),
|
||||||
|
] as const
|
||||||
|
})
|
||||||
|
|
||||||
|
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp | "finder" }))
|
||||||
|
const [menu, setMenu] = createStore({ open: false })
|
||||||
|
const [openRequest, setOpenRequest] = createStore({
|
||||||
|
app: undefined as OpenApp | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
||||||
|
const current = createMemo(
|
||||||
|
() =>
|
||||||
|
options().find((o) => o.id === prefs.app) ??
|
||||||
|
options()[0] ??
|
||||||
|
({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const),
|
||||||
|
)
|
||||||
|
const opening = createMemo(() => openRequest.app !== undefined)
|
||||||
|
|
||||||
|
const selectApp = (app: OpenApp | "finder") => {
|
||||||
|
if (!options().some((item) => item.id === app)) return
|
||||||
|
setPrefs("app", app)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDir = (app: OpenApp | "finder") => {
|
||||||
|
if (opening() || !canOpen() || !platform.openPath) return
|
||||||
|
const directory = input.directory()
|
||||||
|
if (!directory) return
|
||||||
|
|
||||||
|
const item = options().find((o) => o.id === app)
|
||||||
|
const openWith = item && "openWith" in item ? item.openWith : undefined
|
||||||
|
setOpenRequest("app", app)
|
||||||
|
platform
|
||||||
|
.openPath(directory, openWith)
|
||||||
|
.catch((err: unknown) => showRequestError(language, err))
|
||||||
|
.finally(() => {
|
||||||
|
setOpenRequest("app", undefined)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyPath = () => {
|
||||||
|
const directory = input.directory()
|
||||||
|
if (!directory) return
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(directory)
|
||||||
|
.then(() => {
|
||||||
|
showToast({
|
||||||
|
variant: "success",
|
||||||
|
icon: "circle-check",
|
||||||
|
title: language.t("session.share.copy.copied"),
|
||||||
|
description: directory,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => showRequestError(language, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canOpen,
|
||||||
|
opening,
|
||||||
|
current,
|
||||||
|
options,
|
||||||
|
menu,
|
||||||
|
setMenu,
|
||||||
|
openDir,
|
||||||
|
selectApp,
|
||||||
|
copyPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { createMemo, Show } from "solid-js"
|
||||||
|
import type { JSX } from "solid-js"
|
||||||
|
import { useSortable } from "@dnd-kit/solid/sortable"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
|
import { useFile } from "@/context/file"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useCommand } from "@/context/command"
|
||||||
|
import { FileVisual } from "./session-sortable-tab"
|
||||||
|
|
||||||
|
export function SortableTabV2(props: {
|
||||||
|
tab: string
|
||||||
|
index: () => number
|
||||||
|
temporary?: boolean
|
||||||
|
onTabClose: (tab: string) => void
|
||||||
|
onTabDoubleClick?: (tab: string) => void
|
||||||
|
}): JSX.Element {
|
||||||
|
const file = useFile()
|
||||||
|
const language = useLanguage()
|
||||||
|
const command = useCommand()
|
||||||
|
const sortable = useSortable({
|
||||||
|
get id() {
|
||||||
|
return props.tab
|
||||||
|
},
|
||||||
|
get index() {
|
||||||
|
return props.index()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const path = createMemo(() => file.pathFromTab(props.tab))
|
||||||
|
const content = createMemo(() => {
|
||||||
|
const value = path()
|
||||||
|
if (!value) return
|
||||||
|
return <FileVisual path={value} temporary={props.temporary} />
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div ref={sortable.ref} class="h-full flex items-center">
|
||||||
|
<div class="relative">
|
||||||
|
<Tabs.Trigger
|
||||||
|
value={props.tab}
|
||||||
|
closeButton={
|
||||||
|
<TooltipKeybind
|
||||||
|
title={language.t("common.closeTab")}
|
||||||
|
keybind={command.keybind("tab.close")}
|
||||||
|
placement="bottom"
|
||||||
|
gutter={10}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
icon="close-small"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-5 w-5"
|
||||||
|
onClick={() => props.onTabClose(props.tab)}
|
||||||
|
aria-label={language.t("common.closeTab")}
|
||||||
|
/>
|
||||||
|
</TooltipKeybind>
|
||||||
|
}
|
||||||
|
hideCloseButton
|
||||||
|
onMiddleClick={() => props.onTabClose(props.tab)}
|
||||||
|
onDblClick={() => props.onTabDoubleClick?.(props.tab)}
|
||||||
|
>
|
||||||
|
<Show when={content()}>{(value) => value()}</Show>
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -47,12 +47,20 @@ export function getAvatarColors(key?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getProjectAvatarVariant(key?: string): ProjectAvatarVariant {
|
export function getProjectAvatarVariant(key?: string): ProjectAvatarVariant {
|
||||||
if (key === "orange") return "orange"
|
|
||||||
if (key === "pink") return "pink"
|
|
||||||
if (key === "cyan") return "cyan"
|
|
||||||
if (key === "purple") return "purple"
|
|
||||||
if (key === "mint") return "cyan"
|
if (key === "mint") return "cyan"
|
||||||
if (key === "lime") return "green"
|
if (key === "lime") return "green"
|
||||||
|
if (
|
||||||
|
key === "orange" ||
|
||||||
|
key === "yellow" ||
|
||||||
|
key === "cyan" ||
|
||||||
|
key === "green" ||
|
||||||
|
key === "red" ||
|
||||||
|
key === "pink" ||
|
||||||
|
key === "blue" ||
|
||||||
|
key === "purple" ||
|
||||||
|
key === "gray"
|
||||||
|
)
|
||||||
|
return key
|
||||||
return "gray"
|
return "gray"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ type PlatformBase = {
|
|||||||
/** Open a local path in a local app (desktop only) */
|
/** Open a local path in a local app (desktop only) */
|
||||||
openPath?(path: string, app?: string): Promise<void>
|
openPath?(path: string, app?: string): Promise<void>
|
||||||
|
|
||||||
|
/** Reveal a local path in the system file manager; false when the path does not exist (desktop only) */
|
||||||
|
revealPath?(path: string): Promise<boolean>
|
||||||
|
|
||||||
/** Restart the app */
|
/** Restart the app */
|
||||||
restart(): Promise<void>
|
restart(): Promise<void>
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ function contextItemKey(item: ContextItem) {
|
|||||||
return `${key}:c=${digest.slice(0, 8)}`
|
return `${key}:c=${digest.slice(0, 8)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCommentItem(item: ContextItem | (ContextItem & { key: string })) {
|
export function isCommentItem(item: ContextItem | (ContextItem & { key: string })) {
|
||||||
return item.type === "file" && !!item.comment?.trim()
|
return item.type === "file" && !!item.comment?.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export {
|
|||||||
createPromptSession,
|
createPromptSession,
|
||||||
createPromptState,
|
createPromptState,
|
||||||
DEFAULT_PROMPT,
|
DEFAULT_PROMPT,
|
||||||
|
isCommentItem,
|
||||||
isPromptEqual,
|
isPromptEqual,
|
||||||
} from "./prompt-state"
|
} from "./prompt-state"
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -638,7 +638,7 @@ export const dict = {
|
|||||||
"session.error.notFound.description": "This tab points to a session that no longer exists on this server.",
|
"session.error.notFound.description": "This tab points to a session that no longer exists on this server.",
|
||||||
"session.error.notFound.closeTab": "Close Tab",
|
"session.error.notFound.closeTab": "Close Tab",
|
||||||
"session.error.serverConnection": "Can't connect to this server",
|
"session.error.serverConnection": "Can't connect to this server",
|
||||||
"session.review.filesChanged": "{{count}} Files Changed",
|
"session.review.filesChanged": "Files Changed {{count}}",
|
||||||
"session.review.change.one": "Change",
|
"session.review.change.one": "Change",
|
||||||
"session.review.change.other": "Changes",
|
"session.review.change.other": "Changes",
|
||||||
"session.review.loadingChanges": "Loading changes...",
|
"session.review.loadingChanges": "Loading changes...",
|
||||||
|
|||||||
@@ -2,11 +2,30 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import { shouldOpenSessionInBackground } from "./home-session-open"
|
import { shouldOpenSessionInBackground } from "./home-session-open"
|
||||||
|
|
||||||
describe("shouldOpenSessionInBackground", () => {
|
describe("shouldOpenSessionInBackground", () => {
|
||||||
|
test("opens middle clicks in the background", () => {
|
||||||
|
expect(
|
||||||
|
shouldOpenSessionInBackground({ button: 1, mac: true, meta: false, ctrl: false, shift: false, alt: false }),
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
shouldOpenSessionInBackground({ button: 2, mac: true, meta: false, ctrl: false, shift: false, alt: false }),
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
test("requires only the platform primary modifier", () => {
|
test("requires only the platform primary modifier", () => {
|
||||||
expect(shouldOpenSessionInBackground({ mac: true, meta: true, ctrl: false, shift: false, alt: false })).toBe(true)
|
expect(
|
||||||
expect(shouldOpenSessionInBackground({ mac: false, meta: false, ctrl: true, shift: false, alt: false })).toBe(true)
|
shouldOpenSessionInBackground({ button: 0, mac: true, meta: true, ctrl: false, shift: false, alt: false }),
|
||||||
expect(shouldOpenSessionInBackground({ mac: true, meta: true, ctrl: false, shift: true, alt: false })).toBe(false)
|
).toBe(true)
|
||||||
expect(shouldOpenSessionInBackground({ mac: false, meta: false, ctrl: true, shift: false, alt: true })).toBe(false)
|
expect(
|
||||||
expect(shouldOpenSessionInBackground({ mac: false, meta: true, ctrl: false, shift: false, alt: false })).toBe(false)
|
shouldOpenSessionInBackground({ button: 0, mac: false, meta: false, ctrl: true, shift: false, alt: false }),
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
shouldOpenSessionInBackground({ button: 0, mac: true, meta: true, ctrl: false, shift: true, alt: false }),
|
||||||
|
).toBe(false)
|
||||||
|
expect(
|
||||||
|
shouldOpenSessionInBackground({ button: 0, mac: false, meta: false, ctrl: true, shift: false, alt: true }),
|
||||||
|
).toBe(false)
|
||||||
|
expect(
|
||||||
|
shouldOpenSessionInBackground({ button: 0, mac: false, meta: true, ctrl: false, shift: false, alt: false }),
|
||||||
|
).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
export function shouldOpenSessionInBackground(input: {
|
export function shouldOpenSessionInBackground(input: {
|
||||||
|
button: number
|
||||||
mac: boolean
|
mac: boolean
|
||||||
meta: boolean
|
meta: boolean
|
||||||
ctrl: boolean
|
ctrl: boolean
|
||||||
shift: boolean
|
shift: boolean
|
||||||
alt: boolean
|
alt: boolean
|
||||||
}) {
|
}) {
|
||||||
|
if (input.button === 1) return true
|
||||||
|
if (input.button !== 0) return false
|
||||||
if (input.shift || input.alt) return false
|
if (input.shift || input.alt) return false
|
||||||
if (input.mac) return input.meta && !input.ctrl
|
if (input.mac) return input.meta && !input.ctrl
|
||||||
return input.ctrl && !input.meta
|
return input.ctrl && !input.meta
|
||||||
|
|||||||
@@ -240,10 +240,11 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
|||||||
return { setViewport, setContentRef, setHeaderRef, update, titleOpacity }
|
return { setViewport, setContentRef, setHeaderRef, update, titleOpacity }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cmd+click on macOS (Ctrl+click elsewhere) opens a session tab in the
|
// Middle-click or Cmd+click on macOS (Ctrl+click elsewhere) opens a session
|
||||||
// background without navigating, matching browser conventions.
|
// tab in the background without navigating, matching browser conventions.
|
||||||
function isBackgroundOpen(event: MouseEvent) {
|
function isBackgroundOpen(event: MouseEvent) {
|
||||||
return shouldOpenSessionInBackground({
|
return shouldOpenSessionInBackground({
|
||||||
|
button: event.button,
|
||||||
mac: typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform),
|
mac: typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform),
|
||||||
meta: event.metaKey,
|
meta: event.metaKey,
|
||||||
ctrl: event.ctrlKey,
|
ctrl: event.ctrlKey,
|
||||||
@@ -465,8 +466,8 @@ export function NewHome() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function editProject(conn: ServerConnection.Any, project: LocalProject) {
|
function editProject(conn: ServerConnection.Any, project: LocalProject) {
|
||||||
void import("@/components/dialog-edit-project").then((x) => {
|
void import("@/components/dialog-edit-project-v2").then((x) => {
|
||||||
dialog.show(() => <x.DialogEditProject server={conn} project={project} />)
|
void dialog.show(() => <x.DialogEditProjectV2 server={conn} project={project} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1386,7 +1387,15 @@ function HomeSessionSearchResultRow(props: {
|
|||||||
group: !!showProjectName(),
|
group: !!showProjectName(),
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => props.onHighlight()}
|
onMouseEnter={() => props.onHighlight()}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
if (event.button === 1) event.preventDefault()
|
||||||
|
}}
|
||||||
onClick={(event) => props.onSelect(props.record.session, { background: isBackgroundOpen(event) })}
|
onClick={(event) => props.onSelect(props.record.session, { background: isBackgroundOpen(event) })}
|
||||||
|
onAuxClick={(event) => {
|
||||||
|
if (!isBackgroundOpen(event)) return
|
||||||
|
event.preventDefault()
|
||||||
|
props.onSelect(props.record.session, { background: true })
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<HomeSessionLeading
|
<HomeSessionLeading
|
||||||
project={props.record.project}
|
project={props.record.project}
|
||||||
@@ -1446,7 +1455,15 @@ function HomeSessionRow(props: {
|
|||||||
type="button"
|
type="button"
|
||||||
data-component="home-session-row"
|
data-component="home-session-row"
|
||||||
class={`${HOME_ROW} h-10 min-w-0 flex-1 gap-2 py-3 pl-3 pr-10`}
|
class={`${HOME_ROW} h-10 min-w-0 flex-1 gap-2 py-3 pl-3 pr-10`}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
if (event.button === 1) event.preventDefault()
|
||||||
|
}}
|
||||||
onClick={(event) => props.openSession(props.record.session, { background: isBackgroundOpen(event) })}
|
onClick={(event) => props.openSession(props.record.session, { background: isBackgroundOpen(event) })}
|
||||||
|
onAuxClick={(event) => {
|
||||||
|
if (!isBackgroundOpen(event)) return
|
||||||
|
event.preventDefault()
|
||||||
|
props.openSession(props.record.session, { background: true })
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<HomeSessionLeading
|
<HomeSessionLeading
|
||||||
project={props.record.project}
|
project={props.record.project}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
import type { FilePart, Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||||
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query"
|
import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query"
|
||||||
import {
|
import {
|
||||||
@@ -79,6 +80,7 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
|||||||
import { sessionPanelLayout } from "@/pages/session/session-panel-layout"
|
import { sessionPanelLayout } from "@/pages/session/session-panel-layout"
|
||||||
import { SessionReviewEmptyChangesV2 } from "@opencode-ai/session-ui/v2/session-review-empty-changes-v2"
|
import { SessionReviewEmptyChangesV2 } from "@opencode-ai/session-ui/v2/session-review-empty-changes-v2"
|
||||||
import { SessionReviewEmptyNoGitV2 } from "@opencode-ai/session-ui/v2/session-review-empty-no-git-v2"
|
import { SessionReviewEmptyNoGitV2 } from "@opencode-ai/session-ui/v2/session-review-empty-no-git-v2"
|
||||||
|
import { SessionReviewV2SidebarToggle } from "@opencode-ai/session-ui/v2/session-review-v2"
|
||||||
import { ReviewPanelV2 } from "@/pages/session/v2/review-panel-v2"
|
import { ReviewPanelV2 } from "@/pages/session/v2/review-panel-v2"
|
||||||
import { createReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state"
|
import { createReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state"
|
||||||
import { reviewDiffDirectory, reviewDiffNeedsLoad, reviewRootDirectory } from "@/pages/session/v2/review-diff-kinds"
|
import { reviewDiffDirectory, reviewDiffNeedsLoad, reviewRootDirectory } from "@/pages/session/v2/review-diff-kinds"
|
||||||
@@ -1904,7 +1906,30 @@ export default function Page() {
|
|||||||
.map((item) => ({ id: item.id, text: line(item.id) }))
|
.map((item) => ({ id: item.id, text: line(item.id) }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const actions = { revert }
|
// attachment bytes are embedded as a data URL, so downloading always works;
|
||||||
|
// revealing requires the on-disk path captured by the client that attached the file
|
||||||
|
const openAttachment = (file: FilePart) => {
|
||||||
|
const download = () => {
|
||||||
|
const anchor = document.createElement("a")
|
||||||
|
anchor.href = file.url
|
||||||
|
anchor.download = getFilename(file.filename) || "attachment"
|
||||||
|
anchor.click()
|
||||||
|
}
|
||||||
|
const path = file.filename ?? ""
|
||||||
|
const absolute = path.startsWith("/") || path.startsWith("\\\\") || /^[a-zA-Z]:[\\/]/.test(path)
|
||||||
|
if (platform.revealPath && absolute) {
|
||||||
|
void platform.revealPath(path).then(
|
||||||
|
(revealed) => {
|
||||||
|
if (!revealed) download()
|
||||||
|
},
|
||||||
|
() => download(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
download()
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = { revert, openAttachment }
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const sessionID = params.id
|
const sessionID = params.id
|
||||||
@@ -2276,6 +2301,13 @@ export default function Page() {
|
|||||||
reviewHasFocusableContent={() => hasReview() || reviewV2State.sidebarOpened()}
|
reviewHasFocusableContent={() => hasReview() || reviewV2State.sidebarOpened()}
|
||||||
reviewCount={reviewCount}
|
reviewCount={reviewCount}
|
||||||
reviewPanel={reviewPanelV2}
|
reviewPanel={reviewPanelV2}
|
||||||
|
reviewSidebarToggle={(disabled) => (
|
||||||
|
<SessionReviewV2SidebarToggle
|
||||||
|
opened={reviewV2State.sidebarOpened()}
|
||||||
|
disabled={disabled}
|
||||||
|
onToggle={reviewV2State.toggleSidebar}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
fileBrowserState={reviewV2State}
|
fileBrowserState={reviewV2State}
|
||||||
activeDiff={activeReviewFile()}
|
activeDiff={activeReviewFile()}
|
||||||
focusReviewDiff={focusReviewDiff}
|
focusReviewDiff={focusReviewDiff}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createEffect, createMemo, createSignal, Match, on, onCleanup, Switch } from "solid-js"
|
import { createEffect, createMemo, createSignal, Match, on, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
@@ -6,9 +6,12 @@ import type { FileSearchHandle } from "@opencode-ai/session-ui/file"
|
|||||||
import { useFileComponent } from "@opencode-ai/ui/context/file"
|
import { useFileComponent } from "@opencode-ai/ui/context/file"
|
||||||
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
|
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
|
||||||
import { createLineCommentController } from "@opencode-ai/session-ui/line-comment-annotations"
|
import { createLineCommentController } from "@opencode-ai/session-ui/line-comment-annotations"
|
||||||
|
import { createLineCommentControllerV2 } from "@opencode-ai/session-ui/v2/line-comment-annotations-v2"
|
||||||
import { sampledChecksum } from "@opencode-ai/core/util/encode"
|
import { sampledChecksum } from "@opencode-ai/core/util/encode"
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2"
|
||||||
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
@@ -16,6 +19,7 @@ import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange
|
|||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { getSessionHandoff } from "@/pages/session/handoff"
|
import { getSessionHandoff } from "@/pages/session/handoff"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
@@ -53,6 +57,30 @@ function FileCommentMenu(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FileCommentMenuV2(props: {
|
||||||
|
moreLabel: string
|
||||||
|
editLabel: string
|
||||||
|
deleteLabel: string
|
||||||
|
onEdit: VoidFunction
|
||||||
|
onDelete: VoidFunction
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
|
||||||
|
<MenuV2 gutter={4}>
|
||||||
|
<MenuV2.Trigger as="button" type="button" data-slot="line-comment-v2-overflow" aria-label={props.moreLabel}>
|
||||||
|
<LineCommentV2OverflowIcon />
|
||||||
|
</MenuV2.Trigger>
|
||||||
|
<MenuV2.Portal>
|
||||||
|
<MenuV2.Content>
|
||||||
|
<MenuV2.Item onSelect={props.onEdit}>{props.editLabel}</MenuV2.Item>
|
||||||
|
<MenuV2.Item onSelect={props.onDelete}>{props.deleteLabel}</MenuV2.Item>
|
||||||
|
</MenuV2.Content>
|
||||||
|
</MenuV2.Portal>
|
||||||
|
</MenuV2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
type ScrollPos = { x: number; y: number }
|
type ScrollPos = { x: number; y: number }
|
||||||
|
|
||||||
function createScrollSync(input: { tab: () => string; view: ReturnType<typeof useSessionLayout>["view"] }) {
|
function createScrollSync(input: { tab: () => string; view: ReturnType<typeof useSessionLayout>["view"] }) {
|
||||||
@@ -180,6 +208,15 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SessionFileView(props: { tab: string }) {
|
export function SessionFileView(props: { tab: string }) {
|
||||||
|
const settings = useSettings()
|
||||||
|
return (
|
||||||
|
<Show when={settings.general.newLayoutDesigns()} fallback={<SessionFileViewV1 tab={props.tab} />}>
|
||||||
|
<SessionFileViewV2 tab={props.tab} />
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SessionFileViewV1(props: { tab: string }) {
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -463,3 +500,294 @@ export function SessionFileView(props: { tab: string }) {
|
|||||||
|
|
||||||
return content()
|
return content()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SessionFileViewV2(props: { tab: string }) {
|
||||||
|
const file = useFile()
|
||||||
|
const comments = useComments()
|
||||||
|
const language = useLanguage()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const fileComponent = useFileComponent()
|
||||||
|
const { sessionKey, tabs, view } = useSessionLayout()
|
||||||
|
const activeFileTab = createSessionTabs({
|
||||||
|
tabs,
|
||||||
|
pathFromTab: file.pathFromTab,
|
||||||
|
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
||||||
|
}).activeFileTab
|
||||||
|
|
||||||
|
let find: FileSearchHandle | null = null
|
||||||
|
|
||||||
|
const search = {
|
||||||
|
register: (handle: FileSearchHandle | null) => {
|
||||||
|
find = handle
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = createMemo(() => file.pathFromTab(props.tab))
|
||||||
|
const state = createMemo(() => {
|
||||||
|
const p = path()
|
||||||
|
if (!p) return
|
||||||
|
return file.get(p)
|
||||||
|
})
|
||||||
|
const contents = createMemo(() => state()?.content?.content ?? "")
|
||||||
|
const cacheKey = createMemo(() => sampledChecksum(contents()))
|
||||||
|
const selectedLines = createMemo<SelectedLineRange | null>(() => {
|
||||||
|
const p = path()
|
||||||
|
if (!p) return null
|
||||||
|
if (file.ready()) return (file.selectedLines(p) as SelectedLineRange | undefined) ?? null
|
||||||
|
return (getSessionHandoff(sessionKey())?.files[p] as SelectedLineRange | undefined) ?? null
|
||||||
|
})
|
||||||
|
const scrollSync = createScrollSync({
|
||||||
|
tab: () => props.tab,
|
||||||
|
view,
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectionPreview = (source: string, selection: FileSelection) => {
|
||||||
|
return previewSelectedLines(source, {
|
||||||
|
start: selection.startLine,
|
||||||
|
end: selection.endLine,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildPreview = (filePath: string, selection: FileSelection) => {
|
||||||
|
const source = filePath === path() ? contents() : file.get(filePath)?.content?.content
|
||||||
|
if (!source) return undefined
|
||||||
|
return selectionPreview(source, selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addCommentToContext = (input: {
|
||||||
|
file: string
|
||||||
|
selection: SelectedLineRange
|
||||||
|
comment: string
|
||||||
|
preview?: string
|
||||||
|
origin?: "review" | "file"
|
||||||
|
}) => {
|
||||||
|
const selection = selectionFromLines(input.selection)
|
||||||
|
const preview = input.preview ?? buildPreview(input.file, selection)
|
||||||
|
|
||||||
|
const saved = comments.add({
|
||||||
|
file: input.file,
|
||||||
|
selection: input.selection,
|
||||||
|
comment: input.comment,
|
||||||
|
})
|
||||||
|
prompt.context.add({
|
||||||
|
type: "file",
|
||||||
|
path: input.file,
|
||||||
|
selection,
|
||||||
|
comment: input.comment,
|
||||||
|
commentID: saved.id,
|
||||||
|
commentOrigin: input.origin,
|
||||||
|
preview,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCommentInContext = (input: {
|
||||||
|
id: string
|
||||||
|
file: string
|
||||||
|
selection: SelectedLineRange
|
||||||
|
comment: string
|
||||||
|
}) => {
|
||||||
|
comments.update(input.file, input.id, input.comment)
|
||||||
|
const preview = input.file === path() ? buildPreview(input.file, selectionFromLines(input.selection)) : undefined
|
||||||
|
prompt.context.updateComment(input.file, input.id, {
|
||||||
|
comment: input.comment,
|
||||||
|
...(preview ? { preview } : {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeCommentFromContext = (input: { id: string; file: string }) => {
|
||||||
|
comments.remove(input.file, input.id)
|
||||||
|
prompt.context.removeComment(input.file, input.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileComments = createMemo(() => {
|
||||||
|
const p = path()
|
||||||
|
if (!p) return []
|
||||||
|
return comments.list(p)
|
||||||
|
})
|
||||||
|
|
||||||
|
const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection))
|
||||||
|
|
||||||
|
const [note, setNote] = createStore({
|
||||||
|
openedComment: null as string | null,
|
||||||
|
commenting: null as SelectedLineRange | null,
|
||||||
|
selected: null as SelectedLineRange | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const syncSelected = (range: SelectedLineRange | null) => {
|
||||||
|
const p = path()
|
||||||
|
if (!p) return
|
||||||
|
file.setSelectedLines(p, range ? cloneSelectedLineRange(range) : null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeSelection = () => note.selected ?? selectedLines()
|
||||||
|
|
||||||
|
const commentsUi = createLineCommentControllerV2({
|
||||||
|
comments: fileComments,
|
||||||
|
label: language.t("ui.lineComment.submit"),
|
||||||
|
draftKey: () => path() ?? props.tab,
|
||||||
|
mention: {
|
||||||
|
items: file.searchFilesAndDirectories,
|
||||||
|
},
|
||||||
|
getSide: (range) => range.endSide ?? range.side ?? "additions",
|
||||||
|
state: {
|
||||||
|
opened: () => note.openedComment,
|
||||||
|
setOpened: (id) => setNote("openedComment", id),
|
||||||
|
selected: () => note.selected,
|
||||||
|
setSelected: (range) => setNote("selected", range),
|
||||||
|
commenting: () => note.commenting,
|
||||||
|
setCommenting: (range) => setNote("commenting", range),
|
||||||
|
syncSelected,
|
||||||
|
hoverSelected: syncSelected,
|
||||||
|
},
|
||||||
|
onSubmit: ({ comment, selection }) => {
|
||||||
|
const p = path()
|
||||||
|
if (!p) return
|
||||||
|
addCommentToContext({ file: p, selection, comment, origin: "file" })
|
||||||
|
},
|
||||||
|
onUpdate: ({ id, comment, selection }) => {
|
||||||
|
const p = path()
|
||||||
|
if (!p) return
|
||||||
|
updateCommentInContext({ id, file: p, selection, comment })
|
||||||
|
},
|
||||||
|
onDelete: (comment) => {
|
||||||
|
const p = path()
|
||||||
|
if (!p) return
|
||||||
|
removeCommentFromContext({ id: comment.id, file: p })
|
||||||
|
},
|
||||||
|
editSubmitLabel: language.t("common.save"),
|
||||||
|
renderCommentActions: (_, controls) => (
|
||||||
|
<FileCommentMenuV2
|
||||||
|
moreLabel={language.t("common.moreOptions")}
|
||||||
|
editLabel={language.t("common.edit")}
|
||||||
|
deleteLabel={language.t("common.delete")}
|
||||||
|
onEdit={controls.edit}
|
||||||
|
onDelete={controls.remove}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (activeFileTab() !== props.tab) return
|
||||||
|
if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return
|
||||||
|
if (event.key.toLowerCase() !== "f") return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
find?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
makeEventListener(window, "keydown", onKeyDown, { capture: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
path,
|
||||||
|
() => {
|
||||||
|
commentsUi.note.reset()
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const focus = comments.focus()
|
||||||
|
const p = path()
|
||||||
|
if (!focus || !p) return
|
||||||
|
if (focus.file !== p) return
|
||||||
|
if (activeFileTab() !== props.tab) return
|
||||||
|
|
||||||
|
const target = fileComments().find((comment) => comment.id === focus.id)
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
commentsUi.note.openComment(target.id, target.selection, { cancelDraft: true })
|
||||||
|
requestAnimationFrame(() => comments.clearFocus())
|
||||||
|
})
|
||||||
|
|
||||||
|
let prev = {
|
||||||
|
loaded: false,
|
||||||
|
ready: false,
|
||||||
|
active: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const loaded = !!state()?.loaded
|
||||||
|
const ready = file.ready()
|
||||||
|
const active = activeFileTab() === props.tab
|
||||||
|
const restore = (loaded && !prev.loaded) || (ready && !prev.ready) || (active && loaded && !prev.active)
|
||||||
|
prev = { loaded, ready, active }
|
||||||
|
if (!restore) return
|
||||||
|
scrollSync.queueRestore()
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderFile = (source: string) => (
|
||||||
|
<div class="relative overflow-hidden pb-40">
|
||||||
|
<Dynamic
|
||||||
|
component={fileComponent}
|
||||||
|
mode="text"
|
||||||
|
file={{
|
||||||
|
name: path() ?? "",
|
||||||
|
contents: source,
|
||||||
|
cacheKey: cacheKey(),
|
||||||
|
}}
|
||||||
|
enableLineSelection
|
||||||
|
enableGutterUtility
|
||||||
|
selectedLines={activeSelection()}
|
||||||
|
commentedLines={commentedLines()}
|
||||||
|
onRendered={() => {
|
||||||
|
scrollSync.queueRestore()
|
||||||
|
}}
|
||||||
|
annotations={commentsUi.annotations()}
|
||||||
|
renderAnnotation={commentsUi.renderAnnotation}
|
||||||
|
renderGutterUtility={commentsUi.renderGutterUtility}
|
||||||
|
onLineSelected={(range: SelectedLineRange | null) => {
|
||||||
|
commentsUi.onLineSelected(range)
|
||||||
|
}}
|
||||||
|
onLineSelectionEnd={(range: SelectedLineRange | null) => {
|
||||||
|
if (!range) {
|
||||||
|
commentsUi.note.select(null)
|
||||||
|
commentsUi.note.cancelDraft()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commentsUi.onLineSelectionEnd(range)
|
||||||
|
}}
|
||||||
|
onLineNumberSelectionEnd={(range: SelectedLineRange | null) => {
|
||||||
|
commentsUi.onLineNumberSelectionEnd(range)
|
||||||
|
}}
|
||||||
|
search={search}
|
||||||
|
class="select-text"
|
||||||
|
media={{
|
||||||
|
mode: "auto",
|
||||||
|
path: path(),
|
||||||
|
current: state()?.content,
|
||||||
|
onLoad: scrollSync.queueRestore,
|
||||||
|
onError: (args: { kind: "image" | "audio" | "svg" }) => {
|
||||||
|
if (args.kind !== "svg") return
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("toast.file.loadFailed.title"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const content = () => (
|
||||||
|
<div class="mt-3 relative h-full min-h-0">
|
||||||
|
<ScrollView class="h-full" viewportRef={scrollSync.setViewport} onScroll={scrollSync.handleScroll as any}>
|
||||||
|
<Switch>
|
||||||
|
<Match when={state()?.loaded}>{renderFile(contents())}</Match>
|
||||||
|
<Match when={state()?.loading}>
|
||||||
|
<div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={state()?.error}>{(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}</Match>
|
||||||
|
</Switch>
|
||||||
|
</ScrollView>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return content()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,28 +1,46 @@
|
|||||||
import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js"
|
import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
|
import { DragDropProvider as DndKitProvider, PointerSensor } from "@dnd-kit/solid"
|
||||||
|
import { isSortable } from "@dnd-kit/solid/sortable"
|
||||||
|
import { Accessibility, AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom"
|
||||||
|
import { RestrictToHorizontalAxis } from "@dnd-kit/abstract/modifiers"
|
||||||
|
import { RestrictToElement } from "@dnd-kit/dom/modifiers"
|
||||||
|
import {
|
||||||
|
DragDropProvider,
|
||||||
|
DragDropSensors,
|
||||||
|
DragOverlay,
|
||||||
|
SortableProvider,
|
||||||
|
closestCenter,
|
||||||
|
type DragEvent,
|
||||||
|
} from "@thisbeyond/solid-dnd"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Mark } from "@opencode-ai/ui/logo"
|
import { Mark } from "@opencode-ai/ui/logo"
|
||||||
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||||
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
|
||||||
import FileTree from "@/components/file-tree"
|
import FileTree from "@/components/file-tree"
|
||||||
|
import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
|
||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
|
|
||||||
const reviewTabID = "session-side-panel-review-tab"
|
const reviewTabID = "session-side-panel-review-tab"
|
||||||
const reviewTabPanelID = "session-side-panel-review-tabpanel"
|
const reviewTabPanelID = "session-side-panel-review-tabpanel"
|
||||||
import { SessionContextTab, SortableTab, FileVisual } from "@/components/session"
|
const fileBrowserTabPanelID = "session-side-panel-file-browser-tabpanel"
|
||||||
|
import { SessionContextTab, SortableTab, SortableTabV2, FileVisual } from "@/components/session"
|
||||||
|
import { OpenInAppV2 } from "@/components/session/open-in-app-v2"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useFile, type SelectedLineRange } from "@/context/file"
|
import { useFile, type SelectedLineRange } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
||||||
import { FileTabContent } from "@/pages/session/file-tabs"
|
import { FileTabContent } from "@/pages/session/file-tabs"
|
||||||
@@ -53,6 +71,7 @@ export function SessionSidePanel(props: {
|
|||||||
reviewHasFocusableContent: () => boolean
|
reviewHasFocusableContent: () => boolean
|
||||||
reviewCount: () => number
|
reviewCount: () => number
|
||||||
reviewPanel: () => JSX.Element
|
reviewPanel: () => JSX.Element
|
||||||
|
reviewSidebarToggle?: (disabled: boolean) => JSX.Element
|
||||||
fileBrowserState?: SessionFileBrowserState
|
fileBrowserState?: SessionFileBrowserState
|
||||||
activeDiff?: string
|
activeDiff?: string
|
||||||
focusReviewDiff: (path: string) => void
|
focusReviewDiff: (path: string) => void
|
||||||
@@ -66,7 +85,9 @@ export function SessionSidePanel(props: {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
const sdk = useSDK()
|
||||||
const { sessionKey, tabs, view, params } = useSessionLayout()
|
const { sessionKey, tabs, view, params } = useSessionLayout()
|
||||||
|
const projectDirectory = createMemo(() => sdk().directory)
|
||||||
|
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
const shown = settings.visibility.fileTree
|
const shown = settings.visibility.fileTree
|
||||||
@@ -98,11 +119,9 @@ export function SessionSidePanel(props: {
|
|||||||
return "mix" as const
|
return "mix" as const
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
|
|
||||||
|
|
||||||
const out = new Map<string, "add" | "del" | "mix">()
|
const out = new Map<string, "add" | "del" | "mix">()
|
||||||
for (const diff of diffs()) {
|
for (const diff of diffs()) {
|
||||||
const file = normalize(diff.file)
|
const file = normalizeFileTreeV2Path(diff.file)
|
||||||
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
|
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
|
||||||
|
|
||||||
out.set(file, kind)
|
out.set(file, kind)
|
||||||
@@ -159,6 +178,7 @@ export function SessionSidePanel(props: {
|
|||||||
fileBrowser: () => !!props.fileBrowserState,
|
fileBrowser: () => !!props.fileBrowserState,
|
||||||
})
|
})
|
||||||
const contextOpen = tabState.contextOpen
|
const contextOpen = tabState.contextOpen
|
||||||
|
const openFileOpen = tabState.openFileOpen
|
||||||
const panelTabs = tabState.panelTabs
|
const panelTabs = tabState.panelTabs
|
||||||
const openedTabs = tabState.openedTabs
|
const openedTabs = tabState.openedTabs
|
||||||
const activeTab = tabState.activeTab
|
const activeTab = tabState.activeTab
|
||||||
@@ -176,10 +196,8 @@ export function SessionSidePanel(props: {
|
|||||||
layout.fileTree.setTab("all")
|
layout.fileTree.setTab("all")
|
||||||
}
|
}
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
|
||||||
activeDraggable: undefined as string | undefined,
|
|
||||||
})
|
|
||||||
let fileFilter: HTMLInputElement | undefined
|
let fileFilter: HTMLInputElement | undefined
|
||||||
|
let tabList: HTMLDivElement | undefined
|
||||||
const temporaryTab = tabs().preview
|
const temporaryTab = tabs().preview
|
||||||
const previewTab = (value: string) => {
|
const previewTab = (value: string) => {
|
||||||
const next = normalizeTab(value)
|
const next = normalizeTab(value)
|
||||||
@@ -202,10 +220,26 @@ export function SessionSidePanel(props: {
|
|||||||
}
|
}
|
||||||
const browserTab = createMemo(() => {
|
const browserTab = createMemo(() => {
|
||||||
if (!props.fileBrowserState) return undefined
|
if (!props.fileBrowserState) return undefined
|
||||||
if (activeTab() === SESSION_OPEN_FILE_TAB) return SESSION_OPEN_FILE_TAB
|
const active = activeTab()
|
||||||
|
if (active === SESSION_OPEN_FILE_TAB) return SESSION_OPEN_FILE_TAB
|
||||||
|
if (active && file.pathFromTab(active)) return active
|
||||||
return activeFileTab()
|
return activeFileTab()
|
||||||
})
|
})
|
||||||
const browserKinds = createMemo(() => new Map([...kinds()].filter(([, kind]) => kind !== "mix")))
|
// Keep the file-browser shell mounted while any file tab exists. Kobalte briefly
|
||||||
|
// selects Review while the tab For replaces a preview trigger, which would
|
||||||
|
// otherwise dispose the sidebar and reset scroll.
|
||||||
|
const fileBrowserMounted = createMemo(() => {
|
||||||
|
if (!props.fileBrowserState) return false
|
||||||
|
return openedTabs().length > 0 || openFileOpen() || !!browserTab()
|
||||||
|
})
|
||||||
|
const fileBrowserVisible = createMemo(() => {
|
||||||
|
const active = activeTab()
|
||||||
|
return active !== "review" && active !== "context" && active !== "empty"
|
||||||
|
})
|
||||||
|
const openFileKeybind = createMemo(() => command.keybindParts("file.open"))
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
activeDraggable: undefined as string | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
const handleDragStart = (event: unknown) => {
|
const handleDragStart = (event: unknown) => {
|
||||||
const id = getDraggableId(event)
|
const id = getDraggableId(event)
|
||||||
@@ -291,6 +325,9 @@ export function SessionSidePanel(props: {
|
|||||||
"bg-background-base": !settings.general.newLayoutDesigns(),
|
"bg-background-base": !settings.general.newLayoutDesigns(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Show
|
||||||
|
when={props.fileBrowserState}
|
||||||
|
fallback={
|
||||||
<DragDropProvider
|
<DragDropProvider
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
@@ -411,10 +448,6 @@ export function SessionSidePanel(props: {
|
|||||||
iconSize="large"
|
iconSize="large"
|
||||||
class="!rounded-md"
|
class="!rounded-md"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (props.fileBrowserState) {
|
|
||||||
openFileBrowser()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
void import("@/components/dialog-select-file").then((x) => {
|
void import("@/components/dialog-select-file").then((x) => {
|
||||||
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
|
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
|
||||||
})
|
})
|
||||||
@@ -460,20 +493,7 @@ export function SessionSidePanel(props: {
|
|||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={browserTab()}>
|
<Show when={activeFileTab()} keyed>
|
||||||
<SessionFileBrowserTab
|
|
||||||
tab={browserTab()!}
|
|
||||||
placeholder={browserTab() === SESSION_OPEN_FILE_TAB}
|
|
||||||
active={file.pathFromTab(browserTab()!)}
|
|
||||||
kinds={browserKinds()}
|
|
||||||
state={props.fileBrowserState!}
|
|
||||||
onSelect={(path) => previewTab(file.tab(path))}
|
|
||||||
onSelectPermanent={(path) => openTab(file.tab(path))}
|
|
||||||
filterRef={(element) => (fileFilter = element)}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={!props.fileBrowserState && activeFileTab()} keyed>
|
|
||||||
{(tab) => <FileTabContent tab={tab} />}
|
{(tab) => <FileTabContent tab={tab} />}
|
||||||
</Show>
|
</Show>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -492,6 +512,229 @@ export function SessionSidePanel(props: {
|
|||||||
</Show>
|
</Show>
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DragDropProvider>
|
</DragDropProvider>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DndKitProvider
|
||||||
|
sensors={[
|
||||||
|
PointerSensor.configure({
|
||||||
|
activationConstraints: [new PointerActivationConstraints.Distance({ value: 4 })],
|
||||||
|
preventActivation: (event) =>
|
||||||
|
event.target instanceof Element &&
|
||||||
|
(!!event.target.closest('[data-slot="tabs-trigger-close-button"]') ||
|
||||||
|
!!event.target.closest(".session-review-v2-open-in-app-slot")),
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
modifiers={[
|
||||||
|
RestrictToHorizontalAxis,
|
||||||
|
RestrictToElement.configure({ element: () => tabList ?? null }),
|
||||||
|
]}
|
||||||
|
plugins={(defaults) => [
|
||||||
|
...defaults.filter((plugin) => plugin !== Accessibility),
|
||||||
|
AutoScroller.configure({ acceleration: 8, threshold: { x: 0.05, y: 0 } }),
|
||||||
|
Feedback.configure({ dropAnimation: null }),
|
||||||
|
]}
|
||||||
|
onDragEnd={(event) => {
|
||||||
|
const source = event.operation.source
|
||||||
|
if (event.canceled || !isSortable(source) || source.initialIndex === source.index) return
|
||||||
|
tabs().move(source.id.toString(), source.index)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs value={activeTab()} onChange={activateTab}>
|
||||||
|
<div class="session-review-v2-tabs-bar sticky top-0 shrink-0 flex items-center">
|
||||||
|
<Tabs.List
|
||||||
|
ref={(el: HTMLDivElement) => {
|
||||||
|
tabList = el
|
||||||
|
const stop = createFileTabListSync({ el, contextOpen })
|
||||||
|
onCleanup(stop)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={props.reviewSidebarToggle}>
|
||||||
|
{(toggle) => (
|
||||||
|
<div class="h-full shrink-0 flex items-center justify-center">
|
||||||
|
{toggle()(activeTab() === SESSION_OPEN_FILE_TAB)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<Show when={reviewTab() && props.canReview()}>
|
||||||
|
<Tabs.Trigger
|
||||||
|
value="review"
|
||||||
|
id={reviewTabID}
|
||||||
|
aria-controls={activeTab() === "review" ? reviewTabPanelID : undefined}
|
||||||
|
>
|
||||||
|
{props.hasReview()
|
||||||
|
? language.t("session.review.filesChanged", { count: props.reviewCount() })
|
||||||
|
: language.t("session.tab.review")}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Show>
|
||||||
|
<Show when={contextOpen()}>
|
||||||
|
<Tabs.Trigger
|
||||||
|
value="context"
|
||||||
|
closeButton={
|
||||||
|
<TooltipKeybind
|
||||||
|
title={language.t("common.closeTab")}
|
||||||
|
keybind={command.keybind("tab.close")}
|
||||||
|
placement="bottom"
|
||||||
|
gutter={10}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
icon="close-small"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-5 w-5"
|
||||||
|
onClick={() => tabs().close("context")}
|
||||||
|
aria-label={language.t("common.closeTab")}
|
||||||
|
/>
|
||||||
|
</TooltipKeybind>
|
||||||
|
}
|
||||||
|
hideCloseButton
|
||||||
|
onMiddleClick={() => tabs().close("context")}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<SessionContextUsage variant="indicator" />
|
||||||
|
<div>{language.t("session.tab.context")}</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Show>
|
||||||
|
<For each={panelTabs()}>
|
||||||
|
{(tab) => (
|
||||||
|
<Show
|
||||||
|
when={tab === SESSION_OPEN_FILE_TAB}
|
||||||
|
fallback={
|
||||||
|
<SortableTabV2
|
||||||
|
tab={tab}
|
||||||
|
index={() => tabs().all().indexOf(tab)}
|
||||||
|
temporary={temporaryTab() === tab}
|
||||||
|
onTabClose={tabs().close}
|
||||||
|
onTabDoubleClick={temporaryTab() === tab ? openTab : undefined}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tabs.Trigger
|
||||||
|
value={SESSION_OPEN_FILE_TAB}
|
||||||
|
closeButton={
|
||||||
|
<TooltipKeybind
|
||||||
|
title={language.t("common.closeTab")}
|
||||||
|
keybind={command.keybind("tab.close")}
|
||||||
|
placement="bottom"
|
||||||
|
gutter={10}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
icon="close-small"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-5 w-5"
|
||||||
|
onClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
|
||||||
|
aria-label={language.t("common.closeTab")}
|
||||||
|
/>
|
||||||
|
</TooltipKeybind>
|
||||||
|
}
|
||||||
|
hideCloseButton
|
||||||
|
onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-1.5 italic">
|
||||||
|
<Icon name="open-file" size="small" />
|
||||||
|
<span>{language.t("command.file.open")}</span>
|
||||||
|
</div>
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<div
|
||||||
|
class="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center"
|
||||||
|
classList={{
|
||||||
|
"bg-v2-background-bg-base": settings.general.newLayoutDesigns(),
|
||||||
|
"bg-background-stronger": !settings.general.newLayoutDesigns(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TooltipV2
|
||||||
|
value={
|
||||||
|
<>
|
||||||
|
{language.t("command.file.open")}
|
||||||
|
<Show when={openFileKeybind().length > 0}>
|
||||||
|
<KeybindV2 keys={openFileKeybind()} variant="neutral" />
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
placement="bottom"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<IconButtonV2
|
||||||
|
icon={<Icon name="plus-small" />}
|
||||||
|
variant="ghost-muted"
|
||||||
|
size="large"
|
||||||
|
onClick={() => openFileBrowser()}
|
||||||
|
aria-label={language.t("command.file.open")}
|
||||||
|
/>
|
||||||
|
</TooltipV2>
|
||||||
|
</div>
|
||||||
|
</Tabs.List>
|
||||||
|
<div
|
||||||
|
class="session-review-v2-open-in-app-slot shrink-0 flex items-center pr-3"
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<OpenInAppV2 directory={projectDirectory} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={reviewTab() && props.canReview() && activeTab() === "review"}>
|
||||||
|
<div
|
||||||
|
id={reviewTabPanelID}
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby={reviewTabID}
|
||||||
|
tabIndex={props.reviewHasFocusableContent() ? undefined : 0}
|
||||||
|
data-slot="tabs-content"
|
||||||
|
class="flex flex-col h-full overflow-hidden contain-strict"
|
||||||
|
>
|
||||||
|
{props.reviewPanel()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={activeTab() === "empty"}>
|
||||||
|
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||||
|
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||||
|
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-6">
|
||||||
|
<Mark class="w-14 opacity-10" />
|
||||||
|
<div class="text-14-regular text-text-weak max-w-56">
|
||||||
|
{language.t("session.files.selectToOpen")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={activeTab() === "context"}>
|
||||||
|
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||||
|
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||||
|
<SessionContextTab />
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={fileBrowserMounted()}>
|
||||||
|
<div
|
||||||
|
id={fileBrowserTabPanelID}
|
||||||
|
role="tabpanel"
|
||||||
|
data-slot="tabs-content"
|
||||||
|
class="h-full min-h-0 overflow-hidden"
|
||||||
|
classList={{ hidden: !fileBrowserVisible() }}
|
||||||
|
inert={!fileBrowserVisible() || undefined}
|
||||||
|
>
|
||||||
|
<SessionFileBrowserTab
|
||||||
|
tab={browserTab() ?? activeFileTab() ?? SESSION_OPEN_FILE_TAB}
|
||||||
|
placeholder={
|
||||||
|
(browserTab() ?? activeFileTab() ?? SESSION_OPEN_FILE_TAB) === SESSION_OPEN_FILE_TAB
|
||||||
|
}
|
||||||
|
active={file.pathFromTab(browserTab() ?? activeFileTab() ?? "")}
|
||||||
|
kinds={kinds()}
|
||||||
|
state={props.fileBrowserState!}
|
||||||
|
onSelect={(path) => previewTab(file.tab(path))}
|
||||||
|
onSelectPermanent={(path) => openTab(file.tab(path))}
|
||||||
|
filterRef={(element) => (fileFilter = element)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Tabs>
|
||||||
|
</DndKitProvider>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -519,10 +762,19 @@ export function SessionSidePanel(props: {
|
|||||||
>
|
>
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
||||||
|
<Show
|
||||||
|
when={settings.general.newLayoutDesigns()}
|
||||||
|
fallback={
|
||||||
|
<>
|
||||||
{props.reviewCount()}{" "}
|
{props.reviewCount()}{" "}
|
||||||
{language.t(
|
{language.t(
|
||||||
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{language.t("session.review.filesChanged", { count: props.reviewCount() })}
|
||||||
|
</Show>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
||||||
{language.t("session.files.all")}
|
{language.t("session.files.all")}
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ export function MessageTimeline(props: {
|
|||||||
parts: getMsgParts,
|
parts: getMsgParts,
|
||||||
status: sessionStatus,
|
status: sessionStatus,
|
||||||
showReasoningSummaries: settings.general.showReasoningSummaries,
|
showReasoningSummaries: settings.general.showReasoningSummaries,
|
||||||
|
inlineComments: settings.general.newLayoutDesigns,
|
||||||
})
|
})
|
||||||
const activeMessageID = projection.activeMessageID
|
const activeMessageID = projection.activeMessageID
|
||||||
const assistantMessagesByParent = projection.assistantMessagesByParent
|
const assistantMessagesByParent = projection.assistantMessagesByParent
|
||||||
@@ -1135,6 +1136,10 @@ export function MessageTimeline(props: {
|
|||||||
const m = messageByID().get(userMessageRow().userMessageID)
|
const m = messageByID().get(userMessageRow().userMessageID)
|
||||||
if (m?.role === "user") return m
|
if (m?.role === "user") return m
|
||||||
})
|
})
|
||||||
|
const messageComments = createMemo(() => {
|
||||||
|
if (!settings.general.newLayoutDesigns()) return []
|
||||||
|
return getMsgParts(userMessageRow().userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? [])
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<TimelineRowFrame row={userMessageRow}>
|
<TimelineRowFrame row={userMessageRow}>
|
||||||
<Show when={message()}>
|
<Show when={message()}>
|
||||||
@@ -1146,6 +1151,7 @@ export function MessageTimeline(props: {
|
|||||||
parts={getMsgParts(userMessageRow().userMessageID)}
|
parts={getMsgParts(userMessageRow().userMessageID)}
|
||||||
actions={props.actions}
|
actions={props.actions}
|
||||||
useV2Actions={settings.general.newLayoutDesigns()}
|
useV2Actions={settings.general.newLayoutDesigns()}
|
||||||
|
comments={messageComments()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export function createTimelineProjection(input: {
|
|||||||
parts: (messageID: string) => Part[]
|
parts: (messageID: string) => Part[]
|
||||||
status: Accessor<SessionStatus>
|
status: Accessor<SessionStatus>
|
||||||
showReasoningSummaries: Accessor<boolean>
|
showReasoningSummaries: Accessor<boolean>
|
||||||
|
inlineComments: Accessor<boolean>
|
||||||
}) {
|
}) {
|
||||||
const messageByID = createMemo(() => new Map(input.messages().map((message) => [message.id, message] as const)))
|
const messageByID = createMemo(() => new Map(input.messages().map((message) => [message.id, message] as const)))
|
||||||
const assistantMessagesByParent = createMemo(() => {
|
const assistantMessagesByParent = createMemo(() => {
|
||||||
@@ -59,6 +60,7 @@ export function createTimelineProjection(input: {
|
|||||||
input.showReasoningSummaries(),
|
input.showReasoningSummaries(),
|
||||||
input.status().type,
|
input.status().type,
|
||||||
activeMessageID() === userMessage.id,
|
activeMessageID() === userMessage.id,
|
||||||
|
input.inlineComments(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export namespace Timeline {
|
|||||||
showReasoning: boolean,
|
showReasoning: boolean,
|
||||||
status: SessionStatus["type"],
|
status: SessionStatus["type"],
|
||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
|
// v2 renders comments inside the user message attachments row instead of a strip row
|
||||||
|
inlineComments: boolean,
|
||||||
) {
|
) {
|
||||||
const rows: TimelineRow.TimelineRow[] = []
|
const rows: TimelineRow.TimelineRow[] = []
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ export namespace Timeline {
|
|||||||
: groupParts(assistantPartRefs).map((group) => ({ type: "part" as const, group }))
|
: groupParts(assistantPartRefs).map((group) => ({ type: "part" as const, group }))
|
||||||
if (previousUserMessage) rows.push(new TimelineRow.TurnGap({ userMessageID: userMessage.id }))
|
if (previousUserMessage) rows.push(new TimelineRow.TurnGap({ userMessageID: userMessage.id }))
|
||||||
|
|
||||||
if (comments.length > 0)
|
if (comments.length > 0 && !inlineComments)
|
||||||
rows.push(
|
rows.push(
|
||||||
new TimelineRow.CommentStrip({
|
new TimelineRow.CommentStrip({
|
||||||
userMessageID: userMessage.id,
|
userMessageID: userMessage.id,
|
||||||
@@ -84,7 +86,7 @@ export namespace Timeline {
|
|||||||
rows.push(
|
rows.push(
|
||||||
new TimelineRow.UserMessage({
|
new TimelineRow.UserMessage({
|
||||||
userMessageID: userMessage.id,
|
userMessageID: userMessage.id,
|
||||||
anchor: comments.length === 0,
|
anchor: inlineComments || comments.length === 0,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN,
|
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN,
|
||||||
SessionReviewV2,
|
SessionReviewV2,
|
||||||
SessionReviewV2Sidebar,
|
SessionReviewV2Sidebar,
|
||||||
SessionReviewV2SidebarToggle,
|
|
||||||
} from "@opencode-ai/session-ui/v2/session-review-v2"
|
} from "@opencode-ai/session-ui/v2/session-review-v2"
|
||||||
import { SessionReviewFilePreviewV2 } from "@opencode-ai/session-ui/v2/session-review-file-preview-v2"
|
import { SessionReviewFilePreviewV2 } from "@opencode-ai/session-ui/v2/session-review-file-preview-v2"
|
||||||
import { DiffChanges } from "@opencode-ai/ui/v2/diff-changes-v2"
|
import { DiffChanges } from "@opencode-ai/ui/v2/diff-changes-v2"
|
||||||
@@ -66,6 +65,8 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) {
|
|||||||
)
|
)
|
||||||
const searching = createMemo(() => props.state.filter().trim().length > 0)
|
const searching = createMemo(() => props.state.filter().trim().length > 0)
|
||||||
const kinds = createMemo(() => reviewDiffKinds(diffs()))
|
const kinds = createMemo(() => reviewDiffKinds(diffs()))
|
||||||
|
// Changes-only trees omit "M" — every row is already a change; A/D stay visible.
|
||||||
|
const treeKinds = createMemo(() => new Map([...kinds()].filter(([, kind]) => kind !== "mix")))
|
||||||
const activeDiff = createMemo(() => {
|
const activeDiff = createMemo(() => {
|
||||||
// A focused comment takes over the preview until the preview applies it and
|
// A focused comment takes over the preview until the preview applies it and
|
||||||
// clears the focus; the owner then persists the file as the active selection.
|
// clears the focus; the owner then persists the file as the active selection.
|
||||||
@@ -113,9 +114,6 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) {
|
|||||||
stats={<DiffChanges changes={diffs()} />}
|
stats={<DiffChanges changes={diffs()} />}
|
||||||
empty={props.empty}
|
empty={props.empty}
|
||||||
sidebarOpen={props.state.sidebarOpened()}
|
sidebarOpen={props.state.sidebarOpened()}
|
||||||
sidebarToggle={
|
|
||||||
<SessionReviewV2SidebarToggle opened={props.state.sidebarOpened()} onToggle={props.state.toggleSidebar} />
|
|
||||||
}
|
|
||||||
sidebar={
|
sidebar={
|
||||||
// Always mounted: the sidebar header hosts the changes-mode dropdown,
|
// Always mounted: the sidebar header hosts the changes-mode dropdown,
|
||||||
// which must stay reachable when the current mode has zero diffs.
|
// which must stay reachable when the current mode has zero diffs.
|
||||||
@@ -127,7 +125,7 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) {
|
|||||||
diffs={diffs}
|
diffs={diffs}
|
||||||
filteredFiles={filteredFiles}
|
filteredFiles={filteredFiles}
|
||||||
searching={searching}
|
searching={searching}
|
||||||
kinds={kinds}
|
kinds={treeKinds}
|
||||||
activeDiff={activeDiff}
|
activeDiff={activeDiff}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { createMemo, createSignal, createUniqueId, Show } from "solid-js"
|
import { createMemo, createSignal, createUniqueId, Show } from "solid-js"
|
||||||
import { createQuery } from "@tanstack/solid-query"
|
import { createQuery } from "@tanstack/solid-query"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import {
|
import { SessionFilePanelV2, SessionFilePanelV2Empty } from "@opencode-ai/session-ui/v2/session-file-panel-v2"
|
||||||
SessionFilePanelV2,
|
import { SessionReviewV2Sidebar } from "@opencode-ai/session-ui/v2/session-review-v2"
|
||||||
SessionFilePanelV2Empty,
|
import FileTreeV2, { type Kind } from "@/components/file-tree-v2"
|
||||||
SessionFilePanelV2Title,
|
|
||||||
} from "@opencode-ai/session-ui/v2/session-file-panel-v2"
|
|
||||||
import { SessionReviewV2Sidebar, SessionReviewV2SidebarToggle } from "@opencode-ai/session-ui/v2/session-review-v2"
|
|
||||||
import FileTree, { type Kind } from "@/components/file-tree"
|
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
@@ -93,18 +88,11 @@ export function SessionFileBrowserTab(props: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the sidebar outside Kobalte Tabs.Content: a morphing content value
|
||||||
|
// unmounts the whole panel on every file-tab switch and resets sidebar scroll.
|
||||||
return (
|
return (
|
||||||
<Tabs.Content value={props.tab} class="h-full min-h-0 overflow-hidden">
|
|
||||||
<SessionFilePanelV2
|
<SessionFilePanelV2
|
||||||
toolbar
|
toolbar={false}
|
||||||
toolbarStart={
|
|
||||||
<>
|
|
||||||
<SessionReviewV2SidebarToggle opened={sidebarOpened()} onToggle={props.state.toggleSidebar} />
|
|
||||||
<Show when={!sidebarOpened()}>
|
|
||||||
<SessionFilePanelV2Title>{title()}</SessionFilePanelV2Title>
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
sidebar={
|
sidebar={
|
||||||
<SessionReviewV2Sidebar
|
<SessionReviewV2Sidebar
|
||||||
open={sidebarOpened()}
|
open={sidebarOpened()}
|
||||||
@@ -123,9 +111,7 @@ export function SessionFileBrowserTab(props: {
|
|||||||
<Show
|
<Show
|
||||||
when={query()}
|
when={query()}
|
||||||
fallback={
|
fallback={
|
||||||
<FileTree
|
<FileTreeV2
|
||||||
path=""
|
|
||||||
class="pt-1"
|
|
||||||
active={props.active}
|
active={props.active}
|
||||||
kinds={props.kinds}
|
kinds={props.kinds}
|
||||||
onFileClick={(node) => props.onSelect(node.path)}
|
onFileClick={(node) => props.onSelect(node.path)}
|
||||||
@@ -189,6 +175,5 @@ export function SessionFileBrowserTab(props: {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</SessionFilePanelV2>
|
</SessionFilePanelV2>
|
||||||
</Tabs.Content>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/codemode",
|
"name": "@opencode-ai/codemode",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"description": "Effect-native confined code execution over schema-described tools",
|
"description": "Effect-native confined code execution over schema-described tools",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
"@ai-sdk/alibaba": "1.0.17",
|
"@ai-sdk/alibaba": "1.0.17",
|
||||||
"@ai-sdk/amazon-bedrock": "4.0.112",
|
"@ai-sdk/amazon-bedrock": "4.0.112",
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.88",
|
||||||
"@ai-sdk/cerebras": "2.0.41",
|
"@ai-sdk/cerebras": "2.0.41",
|
||||||
"@ai-sdk/cohere": "3.0.27",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "2.0.41",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.128",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.53",
|
"@ai-sdk/openai": "3.0.84",
|
||||||
"@ai-sdk/openai-compatible": "2.0.41",
|
"@ai-sdk/openai-compatible": "2.0.41",
|
||||||
"@ai-sdk/perplexity": "3.0.26",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "6.10.0",
|
"gitlab-ai-provider": "6.11.1",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
|
|||||||
@@ -44,6 +44,21 @@ const Cost = Schema.Struct({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const ReasoningOption = Schema.Union([
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("effort"),
|
||||||
|
values: Schema.Array(Schema.NullOr(Schema.String)),
|
||||||
|
}),
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("toggle"),
|
||||||
|
}),
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("budget_tokens"),
|
||||||
|
min: Schema.optional(Schema.Finite),
|
||||||
|
max: Schema.optional(Schema.Finite),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
export const Model = Schema.Struct({
|
export const Model = Schema.Struct({
|
||||||
id: Schema.String,
|
id: Schema.String,
|
||||||
name: Schema.String,
|
name: Schema.String,
|
||||||
@@ -53,6 +68,7 @@ export const Model = Schema.Struct({
|
|||||||
reasoning: Schema.Boolean,
|
reasoning: Schema.Boolean,
|
||||||
temperature: Schema.Boolean,
|
temperature: Schema.Boolean,
|
||||||
tool_call: Schema.Boolean,
|
tool_call: Schema.Boolean,
|
||||||
|
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
|
||||||
interleaved: Schema.optional(
|
interleaved: Schema.optional(
|
||||||
Schema.Union([
|
Schema.Union([
|
||||||
Schema.Literal(true),
|
Schema.Literal(true),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://opencode.ai",
|
"homepage": "https://opencode.ai",
|
||||||
|
|||||||
@@ -184,6 +184,16 @@ export function registerIpcHandlers(deps: Deps) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle("reveal-path", async (_event: IpcMainInvokeEvent, path: string) => {
|
||||||
|
const exists = await stat(path).then(
|
||||||
|
() => true,
|
||||||
|
() => false,
|
||||||
|
)
|
||||||
|
if (!exists) return false
|
||||||
|
shell.showItemInFolder(path)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle("read-clipboard-image", () => {
|
ipcMain.handle("read-clipboard-image", () => {
|
||||||
const image = clipboard.readImage()
|
const image = clipboard.readImage()
|
||||||
if (image.isEmpty()) return null
|
if (image.isEmpty()) return null
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ const api: ElectronAPI = {
|
|||||||
saveFilePicker: (opts) => ipcRenderer.invoke("save-file-picker", opts),
|
saveFilePicker: (opts) => ipcRenderer.invoke("save-file-picker", opts),
|
||||||
openLink: (url) => ipcRenderer.send("open-link", url),
|
openLink: (url) => ipcRenderer.send("open-link", url),
|
||||||
openPath: (path, app) => ipcRenderer.invoke("open-path", path, app),
|
openPath: (path, app) => ipcRenderer.invoke("open-path", path, app),
|
||||||
|
revealPath: (path) => ipcRenderer.invoke("reveal-path", path),
|
||||||
readClipboardImage: () => ipcRenderer.invoke("read-clipboard-image"),
|
readClipboardImage: () => ipcRenderer.invoke("read-clipboard-image"),
|
||||||
showNotification: (title, body) => ipcRenderer.send("show-notification", title, body),
|
showNotification: (title, body) => ipcRenderer.send("show-notification", title, body),
|
||||||
getWindowFocused: () => ipcRenderer.invoke("get-window-focused"),
|
getWindowFocused: () => ipcRenderer.invoke("get-window-focused"),
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export type ElectronAPI = {
|
|||||||
saveFilePicker: (opts?: { title?: string; defaultPath?: string }) => Promise<string | null>
|
saveFilePicker: (opts?: { title?: string; defaultPath?: string }) => Promise<string | null>
|
||||||
openLink: (url: string) => void
|
openLink: (url: string) => void
|
||||||
openPath: (path: string, app?: string) => Promise<void>
|
openPath: (path: string, app?: string) => Promise<void>
|
||||||
|
revealPath: (path: string) => Promise<boolean>
|
||||||
readClipboardImage: () => Promise<{ buffer: ArrayBuffer; width: number; height: number } | null>
|
readClipboardImage: () => Promise<{ buffer: ArrayBuffer; width: number; height: number } | null>
|
||||||
showNotification: (title: string, body?: string) => void
|
showNotification: (title: string, body?: string) => void
|
||||||
getWindowFocused: () => Promise<boolean>
|
getWindowFocused: () => Promise<boolean>
|
||||||
|
|||||||
@@ -218,6 +218,9 @@ const createPlatform = (windowState: DesktopWindowState): Platform => {
|
|||||||
}
|
}
|
||||||
return window.api.openPath(path, app)
|
return window.api.openPath(path, app)
|
||||||
},
|
},
|
||||||
|
async revealPath(path: string) {
|
||||||
|
return window.api.revealPath(path)
|
||||||
|
},
|
||||||
|
|
||||||
back() {
|
back() {
|
||||||
window.history.back()
|
window.history.back()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"@ai-sdk/alibaba": "1.0.17",
|
"@ai-sdk/alibaba": "1.0.17",
|
||||||
"@ai-sdk/amazon-bedrock": "4.0.112",
|
"@ai-sdk/amazon-bedrock": "4.0.112",
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.88",
|
||||||
"@ai-sdk/cerebras": "2.0.60",
|
"@ai-sdk/cerebras": "2.0.60",
|
||||||
"@ai-sdk/cohere": "3.0.27",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "2.0.41",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.128",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.53",
|
"@ai-sdk/openai": "3.0.84",
|
||||||
"@ai-sdk/openai-compatible": "2.0.41",
|
"@ai-sdk/openai-compatible": "2.0.41",
|
||||||
"@ai-sdk/perplexity": "3.0.26",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "6.10.0",
|
"gitlab-ai-provider": "6.11.1",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
|
|||||||
@@ -348,6 +348,18 @@ const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpCl
|
|||||||
return yield* fetchOrgs(account.url, accessToken)
|
return yield* fetchOrgs(account.url, accessToken)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const remove = Effect.fn("Account.remove")(function* (accountID: AccountID) {
|
||||||
|
const active = yield* repo.active()
|
||||||
|
yield* repo.remove(accountID)
|
||||||
|
if (Option.isNone(active) || active.value.id !== accountID) return
|
||||||
|
|
||||||
|
const next = (yield* orgsByAccount()).flatMap((group) =>
|
||||||
|
group.orgs.map((org) => ({ accountID: group.account.id, orgID: org.id })),
|
||||||
|
)[0]
|
||||||
|
if (!next) return
|
||||||
|
yield* repo.use(next.accountID, Option.some(next.orgID))
|
||||||
|
})
|
||||||
|
|
||||||
const config = Effect.fn("Account.config")(function* (accountID: AccountID, orgID: OrgID) {
|
const config = Effect.fn("Account.config")(function* (accountID: AccountID, orgID: OrgID) {
|
||||||
const resolved = yield* resolveAccess(accountID)
|
const resolved = yield* resolveAccess(accountID)
|
||||||
if (Option.isNone(resolved)) return Option.none()
|
if (Option.isNone(resolved)) return Option.none()
|
||||||
@@ -445,7 +457,7 @@ const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpCl
|
|||||||
activeOrg,
|
activeOrg,
|
||||||
list: repo.list,
|
list: repo.list,
|
||||||
orgsByAccount,
|
orgsByAccount,
|
||||||
remove: repo.remove,
|
remove,
|
||||||
use: repo.use,
|
use: repo.use,
|
||||||
orgs,
|
orgs,
|
||||||
config,
|
config,
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import { setTimeout as sleep } from "node:timers/promises"
|
|||||||
import { createServer } from "http"
|
import { createServer } from "http"
|
||||||
import { OpenAIWebSocketPool } from "./ws-pool"
|
import { OpenAIWebSocketPool } from "./ws-pool"
|
||||||
import { OauthCallbackPage } from "@opencode-ai/core/oauth/page"
|
import { OauthCallbackPage } from "@opencode-ai/core/oauth/page"
|
||||||
|
import { isRecord } from "@/util/record"
|
||||||
|
|
||||||
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||||
const ISSUER = "https://auth.openai.com"
|
const ISSUER = "https://auth.openai.com"
|
||||||
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
|
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
|
||||||
const OAUTH_PORT = 1455
|
const OAUTH_PORT = 1455
|
||||||
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000
|
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000
|
||||||
|
const CODEX_COMPATIBILITY_VERSION = "0.144.0"
|
||||||
|
const RESPONSES_LITE_MODEL = "gpt-5.6-luna"
|
||||||
const ALLOWED_MODELS = new Set(["gpt-5.5", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini"])
|
const ALLOWED_MODELS = new Set(["gpt-5.5", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini"])
|
||||||
const DISALLOWED_MODELS = new Set(["gpt-5.5-pro"])
|
const DISALLOWED_MODELS = new Set(["gpt-5.5-pro"])
|
||||||
|
|
||||||
@@ -283,8 +286,10 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
|||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(provider.models)
|
Object.entries(provider.models)
|
||||||
.filter(([, model]) => {
|
.filter(([, model]) => {
|
||||||
|
if (model.options.reasoningMode === "pro") return false
|
||||||
if (ALLOWED_MODELS.has(model.api.id)) return true
|
if (ALLOWED_MODELS.has(model.api.id)) return true
|
||||||
if (DISALLOWED_MODELS.has(model.api.id)) return false
|
if (DISALLOWED_MODELS.has(model.api.id)) return false
|
||||||
|
if (model.api.id === "gpt-5.6") return false
|
||||||
const match = model.api.id.match(/^gpt-(\d+\.\d+)/)
|
const match = model.api.id.match(/^gpt-(\d+\.\d+)/)
|
||||||
return match ? parseFloat(match[1]) > 5.4 : false
|
return match ? parseFloat(match[1]) > 5.4 : false
|
||||||
})
|
})
|
||||||
@@ -303,6 +308,12 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
|||||||
input: 272_000,
|
input: 272_000,
|
||||||
output: 128_000,
|
output: 128_000,
|
||||||
}
|
}
|
||||||
|
: model.id.includes("gpt-5.6")
|
||||||
|
? {
|
||||||
|
context: 500_000,
|
||||||
|
input: 372_000,
|
||||||
|
output: 128_000,
|
||||||
|
}
|
||||||
: model.limit,
|
: model.limit,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -410,6 +421,9 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
|||||||
|
|
||||||
const requestInit = {
|
const requestInit = {
|
||||||
...init,
|
...init,
|
||||||
|
body: parsed.pathname.endsWith("/responses")
|
||||||
|
? prepareResponsesLiteRequest(init?.body, headers)
|
||||||
|
: init?.body,
|
||||||
headers,
|
headers,
|
||||||
}
|
}
|
||||||
if (websocketFetch && parsed.pathname.endsWith("/responses")) return websocketFetch(url, requestInit)
|
if (websocketFetch && parsed.pathname.endsWith("/responses")) return websocketFetch(url, requestInit)
|
||||||
@@ -554,3 +568,61 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prepareResponsesLiteRequest(body: BodyInit | null | undefined, headers: Headers) {
|
||||||
|
if (typeof body !== "string") return body
|
||||||
|
const request: unknown = JSON.parse(body)
|
||||||
|
if (!isRecord(request)) return body
|
||||||
|
if (request.model !== RESPONSES_LITE_MODEL) return body
|
||||||
|
if (!Array.isArray(request.input)) throw new Error("Responses Lite requires an input array")
|
||||||
|
if (request.tools !== undefined && !Array.isArray(request.tools)) {
|
||||||
|
throw new Error("Responses Lite requires a tools array")
|
||||||
|
}
|
||||||
|
if (request.instructions !== undefined && typeof request.instructions !== "string") {
|
||||||
|
throw new Error("Responses Lite requires string instructions")
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionID = headers.get("session-id")
|
||||||
|
if (!sessionID) throw new Error("Responses Lite requires a session-id header")
|
||||||
|
|
||||||
|
function stripImageDetail(value: unknown) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(stripImageDetail)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isRecord(value)) return
|
||||||
|
if (value.type === "input_image") delete value.detail
|
||||||
|
Object.values(value).forEach(stripImageDetail)
|
||||||
|
}
|
||||||
|
|
||||||
|
stripImageDetail(request.input)
|
||||||
|
request.input = [
|
||||||
|
{ type: "additional_tools", role: "developer", tools: request.tools ?? [] },
|
||||||
|
...(request.instructions
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: "message",
|
||||||
|
role: "developer",
|
||||||
|
content: [{ type: "input_text", text: request.instructions }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...request.input,
|
||||||
|
]
|
||||||
|
delete request.tools
|
||||||
|
delete request.instructions
|
||||||
|
request.tool_choice = "auto"
|
||||||
|
request.parallel_tool_calls = false
|
||||||
|
request.prompt_cache_key = sessionID
|
||||||
|
request.reasoning = {
|
||||||
|
...(isRecord(request.reasoning) ? request.reasoning : {}),
|
||||||
|
context: "all_turns",
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.set("session-id", sessionID)
|
||||||
|
headers.set("x-session-affinity", sessionID)
|
||||||
|
headers.set("version", CODEX_COMPATIBILITY_VERSION)
|
||||||
|
headers.set(OpenAIWebSocketPool.RESPONSES_LITE_HEADER, "true")
|
||||||
|
headers.delete("content-length")
|
||||||
|
return JSON.stringify(request)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { isRecord } from "@/util/record"
|
|||||||
import { OpenAIWebSocket } from "./ws"
|
import { OpenAIWebSocket } from "./ws"
|
||||||
|
|
||||||
export const TITLE_HEADER = "x-opencode-title"
|
export const TITLE_HEADER = "x-opencode-title"
|
||||||
|
export const RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite"
|
||||||
|
const RESPONSES_LITE_CLIENT_METADATA = "ws_request_header_x_openai_internal_codex_responses_lite"
|
||||||
|
|
||||||
export interface CreateWebSocketFetchOptions {
|
export interface CreateWebSocketFetchOptions {
|
||||||
httpFetch?: typeof globalThis.fetch
|
httpFetch?: typeof globalThis.fetch
|
||||||
@@ -98,7 +100,16 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
|
|||||||
})
|
})
|
||||||
const response = OpenAIWebSocket.streamResponsesWebSocket({
|
const response = OpenAIWebSocket.streamResponsesWebSocket({
|
||||||
socket: entry.socket,
|
socket: entry.socket,
|
||||||
body,
|
body:
|
||||||
|
internalHeaders[RESPONSES_LITE_HEADER] === "true"
|
||||||
|
? {
|
||||||
|
...body,
|
||||||
|
client_metadata: {
|
||||||
|
...(isRecord(body.client_metadata) ? body.client_metadata : {}),
|
||||||
|
[RESPONSES_LITE_CLIENT_METADATA]: "true",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: body,
|
||||||
idleTimeout,
|
idleTimeout,
|
||||||
signal: init?.signal ?? undefined,
|
signal: init?.signal ?? undefined,
|
||||||
onFirstEvent: (error) => resolveFirstEvent(error ?? true),
|
onFirstEvent: (error) => resolveFirstEvent(error ?? true),
|
||||||
|
|||||||
@@ -1247,9 +1247,11 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
|
|||||||
variants: {},
|
variants: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const variants = ProviderTransform.reasoningVariants(model, base) ?? ProviderTransform.variants(base)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
variants: mapValues(ProviderTransform.variants(base), (v) => v),
|
variants: mapValues(variants, (v) => v),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1265,14 +1267,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|||||||
id: ModelV2.ID.make(id),
|
id: ModelV2.ID.make(id),
|
||||||
name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
|
name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
|
||||||
cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
|
cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
|
||||||
options: opts.provider?.body
|
options: modeOptions(base, opts.provider?.body),
|
||||||
? Object.fromEntries(
|
|
||||||
Object.entries(opts.provider.body).map(([k, v]) => [
|
|
||||||
k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
|
|
||||||
v,
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
: base.options,
|
|
||||||
headers: opts.provider?.headers ?? base.headers,
|
headers: opts.provider?.headers ?? base.headers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1287,6 +1282,17 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function modeOptions(model: Model, body: Record<string, unknown> | undefined) {
|
||||||
|
if (!body) return model.options
|
||||||
|
const options = Object.fromEntries(
|
||||||
|
Object.entries(body).map(([key, value]) => [key.replace(/_([a-z])/g, (_, char) => char.toUpperCase()), value]),
|
||||||
|
)
|
||||||
|
const reasoning = body.reasoning
|
||||||
|
if (model.api.npm !== "@ai-sdk/openai" || !isRecord(reasoning) || typeof reasoning.mode !== "string") return options
|
||||||
|
const { reasoning: _, ...rest } = options
|
||||||
|
return { ...rest, reasoningMode: reasoning.mode }
|
||||||
|
}
|
||||||
|
|
||||||
function modelSuggestions(provider: Info | undefined, modelID: ModelV2.ID, enableExperimentalModels: boolean) {
|
function modelSuggestions(provider: Info | undefined, modelID: ModelV2.ID, enableExperimentalModels: boolean) {
|
||||||
const available = provider
|
const available = provider
|
||||||
? Object.keys(provider.models).filter((id) => {
|
? Object.keys(provider.models).filter((id) => {
|
||||||
@@ -1492,7 +1498,11 @@ const layer = Layer.effect(
|
|||||||
release_date: model.release_date ?? existingModel?.release_date ?? "",
|
release_date: model.release_date ?? existingModel?.release_date ?? "",
|
||||||
variants: {},
|
variants: {},
|
||||||
}
|
}
|
||||||
const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {})
|
const variants =
|
||||||
|
existingModel?.api.npm === parsedModel.api.npm
|
||||||
|
? (existingModel.variants ?? ProviderTransform.variants(parsedModel))
|
||||||
|
: ProviderTransform.variants(parsedModel)
|
||||||
|
const merged = mergeDeep(variants, model.variants ?? {})
|
||||||
parsedModel.variants = mapValues(
|
parsedModel.variants = mapValues(
|
||||||
pickBy(merged, (v) => !v.disabled),
|
pickBy(merged, (v) => !v.disabled),
|
||||||
(v) => omit(v, ["disabled"]),
|
(v) => omit(v, ["disabled"]),
|
||||||
@@ -1620,7 +1630,7 @@ const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
delete provider.models[modelID]
|
delete provider.models[modelID]
|
||||||
|
|
||||||
if (!model.variants || Object.keys(model.variants).length === 0) {
|
if (model.variants === undefined) {
|
||||||
model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
|
model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -759,7 +759,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "@ai-sdk/gateway":
|
case "@ai-sdk/gateway":
|
||||||
if (model.id.includes("anthropic")) {
|
if (model.api.id.includes("anthropic")) {
|
||||||
if (adaptiveEfforts) {
|
if (adaptiveEfforts) {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
adaptiveEfforts.map((effort) => [
|
adaptiveEfforts.map((effort) => [
|
||||||
@@ -792,8 +792,8 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (model.id.includes("google")) {
|
if (model.api.id.includes("google")) {
|
||||||
if (id.includes("2.5")) {
|
if (model.api.id.includes("2.5")) {
|
||||||
return {
|
return {
|
||||||
high: {
|
high: {
|
||||||
thinkingConfig: {
|
thinkingConfig: {
|
||||||
@@ -804,7 +804,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
max: {
|
max: {
|
||||||
thinkingConfig: {
|
thinkingConfig: {
|
||||||
includeThoughts: true,
|
includeThoughts: true,
|
||||||
thinkingBudget: googleThinkingBudgetMax(id),
|
thinkingBudget: googleThinkingBudgetMax(model.api.id.toLowerCase()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1102,7 +1102,8 @@ export function options(input: {
|
|||||||
input.model.providerID === "openai" ||
|
input.model.providerID === "openai" ||
|
||||||
input.model.api.npm === "@ai-sdk/openai" ||
|
input.model.api.npm === "@ai-sdk/openai" ||
|
||||||
input.model.api.npm === "@ai-sdk/github-copilot" ||
|
input.model.api.npm === "@ai-sdk/github-copilot" ||
|
||||||
input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle"
|
input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle" ||
|
||||||
|
input.model.api.npm === "@ai-sdk/xai"
|
||||||
) {
|
) {
|
||||||
result["store"] = false
|
result["store"] = false
|
||||||
}
|
}
|
||||||
@@ -1257,7 +1258,8 @@ export function smallOptions(model: Provider.Model) {
|
|||||||
if (
|
if (
|
||||||
model.providerID === "openai" ||
|
model.providerID === "openai" ||
|
||||||
model.api.npm === "@ai-sdk/openai" ||
|
model.api.npm === "@ai-sdk/openai" ||
|
||||||
model.api.npm === "@ai-sdk/github-copilot"
|
model.api.npm === "@ai-sdk/github-copilot" ||
|
||||||
|
model.api.npm === "@ai-sdk/xai"
|
||||||
) {
|
) {
|
||||||
const base = { store: false }
|
const base = { store: false }
|
||||||
return mergeDeep(base, small)
|
return mergeDeep(base, small)
|
||||||
@@ -1578,4 +1580,185 @@ export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7
|
|||||||
return schema
|
return schema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function reasoningVariants(model: ModelsDev.Model, target: Provider.Model): Provider.Model["variants"] {
|
||||||
|
const options = model.reasoning_options
|
||||||
|
if (options === undefined) return
|
||||||
|
if (options.length === 0) return {}
|
||||||
|
|
||||||
|
const effort = options.find((option) => option.type === "effort")
|
||||||
|
if (effort) return nonEmptyVariants(effortVariants(target, effort.values))
|
||||||
|
|
||||||
|
const toggle = options.some((option) => option.type === "toggle")
|
||||||
|
const budget = options.find((option) => option.type === "budget_tokens")
|
||||||
|
if (!budget) return toggle ? nonEmptyVariants(reasoningToggle(target)) : undefined
|
||||||
|
|
||||||
|
return nonEmptyVariants({
|
||||||
|
...(toggle ? reasoningToggle(target) : {}),
|
||||||
|
...budgetVariants(target, budget.min, budget.max),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function effortVariants(model: Provider.Model, values: readonly unknown[]) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
values.flatMap((value) => {
|
||||||
|
const id = (() => {
|
||||||
|
if (value === null) return "none"
|
||||||
|
if (typeof value === "string") return value
|
||||||
|
})()
|
||||||
|
if (id === undefined) return []
|
||||||
|
const settings = reasoningEffort(model, id)
|
||||||
|
return settings ? [[id, settings]] : []
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function budgetVariants(model: Provider.Model, min?: number, max?: number) {
|
||||||
|
const maximum = Math.min(max ?? OUTPUT_TOKEN_MAX - 1, model.limit.output - 1, OUTPUT_TOKEN_MAX - 1)
|
||||||
|
if (maximum <= 0) return {}
|
||||||
|
const high = Math.min(Math.max(min ?? 0, Math.floor((maximum + 1) / 2)), maximum)
|
||||||
|
return Object.fromEntries(
|
||||||
|
[
|
||||||
|
{ id: "high", budget: high },
|
||||||
|
{ id: "max", budget: maximum },
|
||||||
|
].flatMap((item) => {
|
||||||
|
const settings = reasoningBudget(model, item.budget)
|
||||||
|
return settings ? [[item.id, settings]] : []
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function nonEmptyVariants(variants: NonNullable<Provider.Model["variants"]>): Provider.Model["variants"] {
|
||||||
|
return Object.keys(variants).length > 0 ? variants : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function reasoningToggle(model: Provider.Model): NonNullable<Provider.Model["variants"]> {
|
||||||
|
if (model.api.npm === "@ai-sdk/alibaba")
|
||||||
|
return {
|
||||||
|
none: { enableThinking: false },
|
||||||
|
high: { enableThinking: true },
|
||||||
|
}
|
||||||
|
if (model.api.npm === "@ai-sdk/cohere")
|
||||||
|
return {
|
||||||
|
none: { thinking: { type: "disabled" } },
|
||||||
|
high: { thinking: { type: "enabled" } },
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reasoningEffort(model: Provider.Model, effort: string) {
|
||||||
|
switch (model.api.npm) {
|
||||||
|
case "@openrouter/ai-sdk-provider":
|
||||||
|
return { reasoning: { effort } }
|
||||||
|
case "@ai-sdk/anthropic":
|
||||||
|
case "@ai-sdk/google-vertex/anthropic":
|
||||||
|
return anthropicEffort(model, effort)
|
||||||
|
case "@ai-sdk/google":
|
||||||
|
case "@ai-sdk/google-vertex":
|
||||||
|
return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
|
||||||
|
case "@ai-sdk/amazon-bedrock":
|
||||||
|
if (anthropicAdaptiveEfforts(model.api.id))
|
||||||
|
return {
|
||||||
|
reasoningConfig: {
|
||||||
|
type: "adaptive",
|
||||||
|
maxReasoningEffort: effort,
|
||||||
|
...(anthropicOmitsThinking(model.api.id) ? { display: "summarized" } : {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (model.api.id.includes("anthropic")) return
|
||||||
|
return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }
|
||||||
|
case "@ai-sdk/gateway":
|
||||||
|
if (model.id.includes("anthropic")) return { thinking: { type: "adaptive", display: "summarized" }, effort }
|
||||||
|
if (model.id.includes("google")) return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
|
||||||
|
return { reasoningEffort: effort }
|
||||||
|
case "@ai-sdk/github-copilot":
|
||||||
|
// OAuth discovery replaces these with variants from Copilot's /models capabilities.
|
||||||
|
if (model.id.includes("gemini")) return
|
||||||
|
if (model.id.includes("claude")) return { reasoningEffort: effort }
|
||||||
|
return { reasoningEffort: effort, reasoningSummary: "auto", include: INCLUDE_ENCRYPTED_REASONING }
|
||||||
|
case "@ai-sdk/openai":
|
||||||
|
case "@ai-sdk/amazon-bedrock/mantle":
|
||||||
|
return { reasoningEffort: effort, reasoningSummary: "auto", include: INCLUDE_ENCRYPTED_REASONING }
|
||||||
|
case "@ai-sdk/azure":
|
||||||
|
return { reasoningEffort: effort, reasoningSummary: "auto", include: INCLUDE_ENCRYPTED_REASONING }
|
||||||
|
case "@jerome-benoit/sap-ai-provider-v2":
|
||||||
|
if (model.id.includes("anthropic"))
|
||||||
|
return { modelParams: { thinking: { type: "adaptive", display: "summarized" }, output_config: { effort } } }
|
||||||
|
return { modelParams: { reasoning_effort: effort } }
|
||||||
|
case "@ai-sdk/openai-compatible":
|
||||||
|
case "@ai-sdk/xai":
|
||||||
|
case "@ai-sdk/mistral":
|
||||||
|
case "@ai-sdk/groq":
|
||||||
|
case "@ai-sdk/cerebras":
|
||||||
|
case "@ai-sdk/deepinfra":
|
||||||
|
case "@ai-sdk/togetherai":
|
||||||
|
case "venice-ai-sdk-provider":
|
||||||
|
case "ai-gateway-provider":
|
||||||
|
return { reasoningEffort: effort }
|
||||||
|
case "@ai-sdk/cohere":
|
||||||
|
case "@ai-sdk/perplexity":
|
||||||
|
case "@ai-sdk/vercel":
|
||||||
|
case "@ai-sdk/alibaba":
|
||||||
|
case "gitlab-ai-provider":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function anthropicEffort(model: Provider.Model, effort: string) {
|
||||||
|
if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) return { effort }
|
||||||
|
if (!anthropicAdaptiveEfforts(model.api.id)) return
|
||||||
|
return {
|
||||||
|
thinking: {
|
||||||
|
type: "adaptive",
|
||||||
|
...(anthropicOmitsThinking(model.api.id) ? { display: "summarized" } : {}),
|
||||||
|
},
|
||||||
|
effort,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reasoningBudget(model: Provider.Model, budget: number) {
|
||||||
|
switch (model.api.npm) {
|
||||||
|
case "@openrouter/ai-sdk-provider":
|
||||||
|
return { reasoning: { max_tokens: budget } }
|
||||||
|
case "@ai-sdk/anthropic":
|
||||||
|
case "@ai-sdk/google-vertex/anthropic":
|
||||||
|
return { thinking: { type: "enabled", budgetTokens: budget } }
|
||||||
|
case "@ai-sdk/google":
|
||||||
|
case "@ai-sdk/google-vertex":
|
||||||
|
return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
|
||||||
|
case "@ai-sdk/amazon-bedrock":
|
||||||
|
return { reasoningConfig: { type: "enabled", budgetTokens: budget } }
|
||||||
|
case "@ai-sdk/gateway":
|
||||||
|
if (model.id.includes("anthropic")) return { thinking: { type: "enabled", budgetTokens: budget } }
|
||||||
|
if (model.id.includes("google")) return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
|
||||||
|
return
|
||||||
|
case "@ai-sdk/cohere":
|
||||||
|
return { thinking: { type: "enabled", tokenBudget: budget } }
|
||||||
|
case "@ai-sdk/alibaba":
|
||||||
|
return { enableThinking: true, thinkingBudget: budget }
|
||||||
|
case "@jerome-benoit/sap-ai-provider-v2":
|
||||||
|
if (model.id.includes("anthropic"))
|
||||||
|
return { modelParams: { thinking: { type: "enabled", budget_tokens: budget } } }
|
||||||
|
if (model.id.includes("gemini"))
|
||||||
|
return { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } } }
|
||||||
|
return
|
||||||
|
case "@ai-sdk/amazon-bedrock/mantle":
|
||||||
|
case "@ai-sdk/azure":
|
||||||
|
case "@ai-sdk/cerebras":
|
||||||
|
case "@ai-sdk/deepinfra":
|
||||||
|
case "@ai-sdk/github-copilot":
|
||||||
|
case "@ai-sdk/groq":
|
||||||
|
case "@ai-sdk/mistral":
|
||||||
|
case "@ai-sdk/openai":
|
||||||
|
case "@ai-sdk/openai-compatible":
|
||||||
|
case "@ai-sdk/perplexity":
|
||||||
|
case "@ai-sdk/togetherai":
|
||||||
|
case "@ai-sdk/vercel":
|
||||||
|
case "@ai-sdk/xai":
|
||||||
|
case "ai-gateway-provider":
|
||||||
|
case "gitlab-ai-provider":
|
||||||
|
case "venice-ai-sdk-provider":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export * as ProviderTransform from "./transform"
|
export * as ProviderTransform from "./transform"
|
||||||
|
|||||||
@@ -173,6 +173,53 @@ it.live("orgsByAccount groups orgs per account", () =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live("remove switches to another org when the active account is removed", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const first = AccountID.make("user-1")
|
||||||
|
const second = AccountID.make("user-2")
|
||||||
|
|
||||||
|
yield* AccountRepo.Service.use((r) =>
|
||||||
|
r.persistAccount({
|
||||||
|
id: first,
|
||||||
|
email: "one@example.com",
|
||||||
|
url: "https://one.example.com",
|
||||||
|
accessToken: AccessToken.make("at_1"),
|
||||||
|
refreshToken: RefreshToken.make("rt_1"),
|
||||||
|
expiry: Date.now() + outsideEagerRefreshWindow,
|
||||||
|
orgID: Option.some(OrgID.make("org-1")),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* AccountRepo.Service.use((r) =>
|
||||||
|
r.persistAccount({
|
||||||
|
id: second,
|
||||||
|
email: "two@example.com",
|
||||||
|
url: "https://two.example.com",
|
||||||
|
accessToken: AccessToken.make("at_2"),
|
||||||
|
refreshToken: RefreshToken.make("rt_2"),
|
||||||
|
expiry: Date.now() + outsideEagerRefreshWindow,
|
||||||
|
orgID: Option.some(OrgID.make("org-2")),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const client = HttpClient.make((req) =>
|
||||||
|
Effect.succeed(
|
||||||
|
req.url === "https://one.example.com/api/orgs" ? json(req, [org("org-1", "One")]) : json(req, [], 404),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Account.use.remove(second).pipe(Effect.provide(live(client)))
|
||||||
|
|
||||||
|
const active = yield* AccountRepo.use.active()
|
||||||
|
expect(Option.getOrThrow(active)).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: first,
|
||||||
|
active_org_id: OrgID.make("org-1"),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.live("token refresh persists the new token", () =>
|
it.live("token refresh persists the new token", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const id = AccountID.make("user-1")
|
const id = AccountID.make("user-1")
|
||||||
|
|||||||
@@ -149,6 +149,49 @@ describe("plugin.codex", () => {
|
|||||||
await enabled.dispose?.()
|
await enabled.dispose?.()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("filters unsupported modes and uses Codex context limits for OAuth GPT models", async () => {
|
||||||
|
const hooks = await CodexAuthPlugin({} as never)
|
||||||
|
const limit = { context: 1_050_000, input: 922_000, output: 128_000 }
|
||||||
|
const provider = {
|
||||||
|
models: {
|
||||||
|
...Object.fromEntries(
|
||||||
|
["gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.7-pro"].map((id) => [
|
||||||
|
id,
|
||||||
|
{ id, api: { id }, limit, cost: {}, options: {} },
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
"gpt-5.4-pro": {
|
||||||
|
id: "gpt-5.4-pro",
|
||||||
|
api: { id: "gpt-5.4" },
|
||||||
|
limit,
|
||||||
|
cost: {},
|
||||||
|
options: { reasoningMode: "pro" },
|
||||||
|
},
|
||||||
|
"gpt-5.6-sol-high": {
|
||||||
|
id: "gpt-5.6-sol-high",
|
||||||
|
api: { id: "gpt-5.6-sol" },
|
||||||
|
limit,
|
||||||
|
cost: {},
|
||||||
|
options: { reasoningEffort: "high" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const models = await hooks.provider!.models!(provider as never, { auth: { type: "oauth" } } as never)
|
||||||
|
|
||||||
|
expect(models["gpt-5.4"]?.limit).toEqual(limit)
|
||||||
|
expect(models["gpt-5.5"]?.limit).toEqual({ context: 400_000, input: 272_000, output: 128_000 })
|
||||||
|
expect(models["gpt-5.6-sol"]?.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 })
|
||||||
|
expect(models["gpt-5.6-terra"]?.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 })
|
||||||
|
expect(models["gpt-5.6-luna"]?.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 })
|
||||||
|
expect(models["gpt-5.4-pro"]).toBeUndefined()
|
||||||
|
expect(models["gpt-5.7-pro"]).toBeDefined()
|
||||||
|
expect(models["gpt-5.6-sol-high"]).toBeDefined()
|
||||||
|
expect(await hooks.provider!.models!(provider as never, { auth: { type: "api" } } as never)).toBe(
|
||||||
|
provider.models as never,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test("deduplicates concurrent Codex token refreshes", async () => {
|
test("deduplicates concurrent Codex token refreshes", async () => {
|
||||||
let auth = {
|
let auth = {
|
||||||
type: "oauth" as const,
|
type: "oauth" as const,
|
||||||
|
|||||||
@@ -159,10 +159,10 @@ it.instance(
|
|||||||
const providers = yield* list
|
const providers = yield* list
|
||||||
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
|
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
|
||||||
const models = Object.keys(providers[ProviderV2.ID.anthropic].models)
|
const models = Object.keys(providers[ProviderV2.ID.anthropic].models)
|
||||||
expect(models).toContain("claude-sonnet-4-20250514")
|
expect(models).toContain("claude-sonnet-4-6")
|
||||||
expect(models.length).toBe(1)
|
expect(models.length).toBe(1)
|
||||||
}),
|
}),
|
||||||
{ config: { provider: { anthropic: { whitelist: ["claude-sonnet-4-20250514"] } } } },
|
{ config: { provider: { anthropic: { whitelist: ["claude-sonnet-4-6"] } } } },
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
@@ -301,10 +301,10 @@ it.instance("getModel returns model for valid provider/model", () =>
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
|
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
const provider = yield* Provider.Service
|
const provider = yield* Provider.Service
|
||||||
const model = yield* provider.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-20250514"))
|
const model = yield* provider.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-6"))
|
||||||
expect(model).toBeDefined()
|
expect(model).toBeDefined()
|
||||||
expect(String(model.providerID)).toBe("anthropic")
|
expect(String(model.providerID)).toBe("anthropic")
|
||||||
expect(String(model.id)).toBe("claude-sonnet-4-20250514")
|
expect(String(model.id)).toBe("claude-sonnet-4-6")
|
||||||
const language = yield* provider.getLanguage(model)
|
const language = yield* provider.getLanguage(model)
|
||||||
expect(language).toBeDefined()
|
expect(language).toBeDefined()
|
||||||
}),
|
}),
|
||||||
@@ -435,7 +435,7 @@ it.instance(
|
|||||||
"model options are merged from existing model",
|
"model options are merged from existing model",
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const providers = yield* list
|
const providers = yield* list
|
||||||
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
|
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
|
||||||
expect(model.options.customOption).toBe("custom-value")
|
expect(model.options.customOption).toBe("custom-value")
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
@@ -443,7 +443,7 @@ it.instance(
|
|||||||
provider: {
|
provider: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
options: { apiKey: "test-api-key" },
|
options: { apiKey: "test-api-key" },
|
||||||
models: { "claude-sonnet-4-20250514": { options: { customOption: "custom-value" } } },
|
models: { "claude-sonnet-4-6": { options: { customOption: "custom-value" } } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -549,7 +549,7 @@ it.instance(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
const providers = yield* list
|
const providers = yield* list
|
||||||
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
|
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
|
||||||
expect(model.name).toBe("Custom Name for Sonnet")
|
expect(model.name).toBe("Custom Name for Sonnet")
|
||||||
expect(model.capabilities.toolcall).toBe(true)
|
expect(model.capabilities.toolcall).toBe(true)
|
||||||
expect(model.capabilities.attachment).toBe(true)
|
expect(model.capabilities.attachment).toBe(true)
|
||||||
@@ -557,7 +557,46 @@ it.instance(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
config: {
|
config: {
|
||||||
provider: { anthropic: { models: { "claude-sonnet-4-20250514": { name: "Custom Name for Sonnet" } } } },
|
provider: { anthropic: { models: { "claude-sonnet-4-6": { name: "Custom Name for Sonnet" } } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
it.instance(
|
||||||
|
"model config preserves explicitly empty models.dev variants",
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* set("OPENAI_API_KEY", "test-api-key")
|
||||||
|
const providers = yield* list
|
||||||
|
const model = providers[ProviderV2.ID.openai].models["custom-gpt-chat"]
|
||||||
|
expect(model.name).toBe("Custom GPT Chat")
|
||||||
|
expect(model.variants).toEqual({})
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
config: {
|
||||||
|
provider: {
|
||||||
|
openai: { models: { "custom-gpt-chat": { id: "gpt-5-chat-latest", name: "Custom GPT Chat" } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
it.instance(
|
||||||
|
"model config regenerates variants when overriding the provider package",
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
|
const providers = yield* list
|
||||||
|
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
|
||||||
|
expect(model.variants?.low).toEqual({ reasoningEffort: "low" })
|
||||||
|
expect(model.variants?.max).toBeUndefined()
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
config: {
|
||||||
|
provider: {
|
||||||
|
anthropic: {
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
models: { "claude-sonnet-4-6": { name: "Claude via OpenAI" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -590,16 +629,16 @@ it.instance(
|
|||||||
const providers = yield* list
|
const providers = yield* list
|
||||||
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
|
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
|
||||||
const models = Object.keys(providers[ProviderV2.ID.anthropic].models)
|
const models = Object.keys(providers[ProviderV2.ID.anthropic].models)
|
||||||
expect(models).toContain("claude-sonnet-4-20250514")
|
expect(models).toContain("claude-sonnet-4-6")
|
||||||
expect(models).not.toContain("claude-opus-4-20250514")
|
expect(models).not.toContain("claude-opus-4-6")
|
||||||
expect(models.length).toBe(1)
|
expect(models.length).toBe(1)
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
config: {
|
config: {
|
||||||
provider: {
|
provider: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
whitelist: ["claude-sonnet-4-20250514", "claude-opus-4-20250514"],
|
whitelist: ["claude-sonnet-4-6", "claude-opus-4-6"],
|
||||||
blacklist: ["claude-opus-4-20250514"],
|
blacklist: ["claude-opus-4-6"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -773,9 +812,9 @@ it.instance(
|
|||||||
const model = yield* Provider.use.getSmallModel(ProviderV2.ID.anthropic)
|
const model = yield* Provider.use.getSmallModel(ProviderV2.ID.anthropic)
|
||||||
expect(model).toBeDefined()
|
expect(model).toBeDefined()
|
||||||
expect(String(model?.providerID)).toBe("anthropic")
|
expect(String(model?.providerID)).toBe("anthropic")
|
||||||
expect(String(model?.id)).toBe("claude-sonnet-4-20250514")
|
expect(String(model?.id)).toBe("claude-sonnet-4-6")
|
||||||
}),
|
}),
|
||||||
{ config: { small_model: "anthropic/claude-sonnet-4-20250514" } },
|
{ config: { small_model: "anthropic/claude-sonnet-4-6" } },
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
@@ -1094,8 +1133,8 @@ it.instance(
|
|||||||
it.instance("getModel returns consistent results", () =>
|
it.instance("getModel returns consistent results", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
const model1 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-20250514"))
|
const model1 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-6"))
|
||||||
const model2 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-20250514"))
|
const model2 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-6"))
|
||||||
expect(model1.providerID).toEqual(model2.providerID)
|
expect(model1.providerID).toEqual(model2.providerID)
|
||||||
expect(model1.id).toEqual(model2.id)
|
expect(model1.id).toEqual(model2.id)
|
||||||
expect(model1).toEqual(model2)
|
expect(model1).toEqual(model2)
|
||||||
@@ -1337,16 +1376,17 @@ it.instance(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
test("mode cost preserves over-200k pricing from base model", () => {
|
test("mode options and cost are derived from the base model", () => {
|
||||||
const provider = {
|
const provider = {
|
||||||
id: "openai",
|
id: "openai",
|
||||||
name: "OpenAI",
|
name: "OpenAI",
|
||||||
env: [],
|
env: [],
|
||||||
|
npm: "@ai-sdk/openai",
|
||||||
api: "https://api.openai.com/v1",
|
api: "https://api.openai.com/v1",
|
||||||
models: {
|
models: {
|
||||||
"gpt-5.4": {
|
"gpt-5.6-sol": {
|
||||||
id: "gpt-5.4",
|
id: "gpt-5.6-sol",
|
||||||
name: "GPT-5.4",
|
name: "GPT-5.6 Sol",
|
||||||
family: "gpt",
|
family: "gpt",
|
||||||
release_date: "2026-03-05",
|
release_date: "2026-03-05",
|
||||||
attachment: true,
|
attachment: true,
|
||||||
@@ -1382,18 +1422,29 @@ test("mode cost preserves over-200k pricing from base model", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pro: {
|
||||||
|
provider: {
|
||||||
|
body: {
|
||||||
|
reasoning: { mode: "pro" },
|
||||||
|
service_tier: "priority",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as ModelsDev.Provider
|
} as unknown as ModelsDev.Provider
|
||||||
|
|
||||||
const model = Provider.fromModelsDevProvider(provider).models["gpt-5.4-fast"]
|
const model = Provider.fromModelsDevProvider(provider).models["gpt-5.6-sol-fast"]
|
||||||
expect(model.cost.input).toEqual(5)
|
expect(model.cost.input).toEqual(5)
|
||||||
expect(model.cost.output).toEqual(30)
|
expect(model.cost.output).toEqual(30)
|
||||||
expect(model.cost.cache.read).toEqual(0.5)
|
expect(model.cost.cache.read).toEqual(0.5)
|
||||||
expect(model.cost.cache.write).toEqual(0)
|
expect(model.cost.cache.write).toEqual(0)
|
||||||
expect(model.options["serviceTier"]).toEqual("priority")
|
expect(model.options["serviceTier"]).toEqual("priority")
|
||||||
|
const pro = Provider.fromModelsDevProvider(provider).models["gpt-5.6-sol-pro"]
|
||||||
|
expect(pro.api.id).toEqual("gpt-5.6-sol")
|
||||||
|
expect(pro.options).toEqual({ reasoningMode: "pro", serviceTier: "priority" })
|
||||||
expect(model.cost.experimentalOver200K).toEqual({
|
expect(model.cost.experimentalOver200K).toEqual({
|
||||||
input: 5,
|
input: 5,
|
||||||
output: 22.5,
|
output: 22.5,
|
||||||
@@ -1426,6 +1477,62 @@ test("models.dev normalization fills required response fields", () => {
|
|||||||
expect(model.release_date).toBe("")
|
expect(model.release_date).toBe("")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("models.dev reasoning options replace generated variants and unsupported options fall back", () => {
|
||||||
|
const provider = {
|
||||||
|
id: "reasoning",
|
||||||
|
name: "Reasoning",
|
||||||
|
env: [],
|
||||||
|
npm: "@ai-sdk/openai",
|
||||||
|
models: {
|
||||||
|
explicit: {
|
||||||
|
id: "gpt-5.4",
|
||||||
|
name: "Explicit",
|
||||||
|
reasoning: true,
|
||||||
|
reasoning_options: [{ type: "effort", values: ["low"] }],
|
||||||
|
limit: { context: 128_000, output: 64_000 },
|
||||||
|
},
|
||||||
|
empty: {
|
||||||
|
id: "gpt-5.4",
|
||||||
|
name: "Empty",
|
||||||
|
reasoning: true,
|
||||||
|
reasoning_options: [],
|
||||||
|
limit: { context: 128_000, output: 64_000 },
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
id: "gpt-5.4",
|
||||||
|
name: "Fallback",
|
||||||
|
reasoning: true,
|
||||||
|
reasoning_options: [{ type: "toggle" }],
|
||||||
|
limit: { context: 128_000, output: 64_000 },
|
||||||
|
},
|
||||||
|
override: {
|
||||||
|
id: "gemini-3-pro",
|
||||||
|
name: "Override",
|
||||||
|
reasoning: true,
|
||||||
|
reasoning_options: [{ type: "effort", values: ["high"] }],
|
||||||
|
provider: { npm: "@ai-sdk/google" },
|
||||||
|
limit: { context: 128_000, output: 64_000 },
|
||||||
|
experimental: { modes: { fast: {} } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as ModelsDev.Provider
|
||||||
|
|
||||||
|
const models = Provider.fromModelsDevProvider(provider).models
|
||||||
|
expect(models.explicit.variants).toEqual({
|
||||||
|
low: {
|
||||||
|
reasoningEffort: "low",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
include: ["reasoning.encrypted_content"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(models.empty.variants).toEqual({})
|
||||||
|
expect(Object.keys(models.fallback.variants ?? {})).toEqual(["none", "low", "medium", "high", "xhigh"])
|
||||||
|
expect(models.override.variants).toEqual({
|
||||||
|
high: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
|
||||||
|
})
|
||||||
|
expect(models["gemini-3-pro-fast"].variants).toEqual(models.override.variants)
|
||||||
|
})
|
||||||
|
|
||||||
test("public provider info omits invalid models", () => {
|
test("public provider info omits invalid models", () => {
|
||||||
const provider = Provider.fromModelsDevProvider({
|
const provider = Provider.fromModelsDevProvider({
|
||||||
id: "test",
|
id: "test",
|
||||||
@@ -1457,7 +1564,7 @@ it.instance("model variants are generated for reasoning models", () =>
|
|||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
const providers = yield* list
|
const providers = yield* list
|
||||||
// Claude sonnet 4 has reasoning capability
|
// Claude sonnet 4 has reasoning capability
|
||||||
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
|
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
|
||||||
expect(model.capabilities.reasoning).toBe(true)
|
expect(model.capabilities.reasoning).toBe(true)
|
||||||
expect(model.variants).toBeDefined()
|
expect(model.variants).toBeDefined()
|
||||||
expect(Object.keys(model.variants!).length).toBeGreaterThan(0)
|
expect(Object.keys(model.variants!).length).toBeGreaterThan(0)
|
||||||
@@ -1469,7 +1576,7 @@ it.instance(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
const providers = yield* list
|
const providers = yield* list
|
||||||
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
|
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
|
||||||
expect(model.variants).toBeDefined()
|
expect(model.variants).toBeDefined()
|
||||||
expect(model.variants!["high"]).toBeUndefined()
|
expect(model.variants!["high"]).toBeUndefined()
|
||||||
// max variant should still exist
|
// max variant should still exist
|
||||||
@@ -1479,7 +1586,7 @@ it.instance(
|
|||||||
config: {
|
config: {
|
||||||
provider: {
|
provider: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
models: { "claude-sonnet-4-20250514": { variants: { high: { disabled: true } } } },
|
models: { "claude-sonnet-4-6": { variants: { high: { disabled: true } } } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1491,7 +1598,7 @@ it.instance(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
const providers = yield* list
|
const providers = yield* list
|
||||||
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
|
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
|
||||||
expect(model.variants!["high"]).toBeDefined()
|
expect(model.variants!["high"]).toBeDefined()
|
||||||
expect(model.variants!["high"].thinking.budgetTokens).toBe(20000)
|
expect(model.variants!["high"].thinking.budgetTokens).toBe(20000)
|
||||||
}),
|
}),
|
||||||
@@ -1500,7 +1607,7 @@ it.instance(
|
|||||||
provider: {
|
provider: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
models: {
|
models: {
|
||||||
"claude-sonnet-4-20250514": {
|
"claude-sonnet-4-6": {
|
||||||
variants: { high: { thinking: { type: "enabled", budgetTokens: 20000 } } },
|
variants: { high: { thinking: { type: "enabled", budgetTokens: 20000 } } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1515,7 +1622,7 @@ it.instance(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
const providers = yield* list
|
const providers = yield* list
|
||||||
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
|
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
|
||||||
expect(model.variants!["max"]).toBeDefined()
|
expect(model.variants!["max"]).toBeDefined()
|
||||||
expect(model.variants!["max"].disabled).toBeUndefined()
|
expect(model.variants!["max"].disabled).toBeUndefined()
|
||||||
expect(model.variants!["max"].customField).toBe("test")
|
expect(model.variants!["max"].customField).toBe("test")
|
||||||
@@ -1525,7 +1632,7 @@ it.instance(
|
|||||||
provider: {
|
provider: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
models: {
|
models: {
|
||||||
"claude-sonnet-4-20250514": {
|
"claude-sonnet-4-6": {
|
||||||
variants: { max: { disabled: false, customField: "test" } },
|
variants: { max: { disabled: false, customField: "test" } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1540,7 +1647,7 @@ it.instance(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
const providers = yield* list
|
const providers = yield* list
|
||||||
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
|
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
|
||||||
expect(model.variants).toBeDefined()
|
expect(model.variants).toBeDefined()
|
||||||
expect(Object.keys(model.variants!).length).toBe(0)
|
expect(Object.keys(model.variants!).length).toBe(0)
|
||||||
}),
|
}),
|
||||||
@@ -1549,8 +1656,13 @@ it.instance(
|
|||||||
provider: {
|
provider: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
models: {
|
models: {
|
||||||
"claude-sonnet-4-20250514": {
|
"claude-sonnet-4-6": {
|
||||||
variants: { high: { disabled: true }, max: { disabled: true } },
|
variants: {
|
||||||
|
low: { disabled: true },
|
||||||
|
medium: { disabled: true },
|
||||||
|
high: { disabled: true },
|
||||||
|
max: { disabled: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1564,7 +1676,7 @@ it.instance(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
const providers = yield* list
|
const providers = yield* list
|
||||||
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
|
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
|
||||||
expect(model.variants!["high"]).toBeDefined()
|
expect(model.variants!["high"]).toBeDefined()
|
||||||
// Should have both the generated thinking config and the custom option
|
// Should have both the generated thinking config and the custom option
|
||||||
expect(model.variants!["high"].thinking).toBeDefined()
|
expect(model.variants!["high"].thinking).toBeDefined()
|
||||||
@@ -1575,7 +1687,7 @@ it.instance(
|
|||||||
provider: {
|
provider: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
models: {
|
models: {
|
||||||
"claude-sonnet-4-20250514": { variants: { high: { extraOption: "custom-value" } } },
|
"claude-sonnet-4-6": { variants: { high: { extraOption: "custom-value" } } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ProviderTransform } from "@/provider/transform"
|
|||||||
import { LLMRequestPrep } from "@/session/llm/request"
|
import { LLMRequestPrep } from "@/session/llm/request"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||||
import { jsonSchema } from "ai"
|
import { jsonSchema } from "ai"
|
||||||
|
|
||||||
describe("ProviderTransform.options - setCacheKey", () => {
|
describe("ProviderTransform.options - setCacheKey", () => {
|
||||||
@@ -155,6 +156,43 @@ describe("ProviderTransform.options - setCacheKey", () => {
|
|||||||
expect(result.store).toBe(false)
|
expect(result.store).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should set store=false for xAI provider by default", () => {
|
||||||
|
const xaiModel = {
|
||||||
|
...mockModel,
|
||||||
|
providerID: "xai",
|
||||||
|
api: {
|
||||||
|
id: "grok-4",
|
||||||
|
url: "https://api.x.ai",
|
||||||
|
npm: "@ai-sdk/xai",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const result = ProviderTransform.options({
|
||||||
|
model: xaiModel,
|
||||||
|
sessionID,
|
||||||
|
providerOptions: {},
|
||||||
|
})
|
||||||
|
expect(result.store).toBe(false)
|
||||||
|
expect(result.promptCacheKey).toBe(sessionID)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should set store=false for xAI SDK regardless of provider ID", () => {
|
||||||
|
const xaiModel = {
|
||||||
|
...mockModel,
|
||||||
|
providerID: "custom-xai",
|
||||||
|
api: {
|
||||||
|
id: "grok-4",
|
||||||
|
url: "https://api.x.ai",
|
||||||
|
npm: "@ai-sdk/xai",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const result = ProviderTransform.options({
|
||||||
|
model: xaiModel,
|
||||||
|
sessionID,
|
||||||
|
providerOptions: {},
|
||||||
|
})
|
||||||
|
expect(result.store).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
test("should set store=false for azure provider by default", () => {
|
test("should set store=false for azure provider by default", () => {
|
||||||
const azureModel = {
|
const azureModel = {
|
||||||
...mockModel,
|
...mockModel,
|
||||||
@@ -2988,6 +3026,268 @@ describe("ProviderTransform.temperature - Cohere North", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("ProviderTransform.reasoningVariants", () => {
|
||||||
|
const model = (reasoning_options: ModelsDev.Model["reasoning_options"]) => ({ reasoning_options }) as ModelsDev.Model
|
||||||
|
const target = (npm: string, id = "test-model") =>
|
||||||
|
({ id, api: { id, npm, url: "" }, capabilities: { reasoning: true }, limit: { output: 64_000 } }) as any
|
||||||
|
|
||||||
|
test("respects explicitly empty reasoning options", () => {
|
||||||
|
expect(ProviderTransform.reasoningVariants(model([]), target("@ai-sdk/openai"))).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
["@openrouter/ai-sdk-provider", { reasoning: { effort: "high" } }],
|
||||||
|
["@ai-sdk/anthropic", { thinking: { type: "adaptive" }, effort: "high" }, "claude-opus-4-6"],
|
||||||
|
[
|
||||||
|
"@ai-sdk/google-vertex/anthropic",
|
||||||
|
{ thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
||||||
|
"claude-opus-4-7",
|
||||||
|
],
|
||||||
|
["@ai-sdk/google", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }],
|
||||||
|
["@ai-sdk/google-vertex", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }],
|
||||||
|
[
|
||||||
|
"@ai-sdk/azure",
|
||||||
|
{
|
||||||
|
reasoningEffort: "high",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
include: ["reasoning.encrypted_content"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@ai-sdk/openai",
|
||||||
|
{
|
||||||
|
reasoningEffort: "high",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
include: ["reasoning.encrypted_content"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@ai-sdk/amazon-bedrock/mantle",
|
||||||
|
{
|
||||||
|
reasoningEffort: "high",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
include: ["reasoning.encrypted_content"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@ai-sdk/github-copilot",
|
||||||
|
{
|
||||||
|
reasoningEffort: "high",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
include: ["reasoning.encrypted_content"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
["@ai-sdk/openai-compatible", { reasoningEffort: "high" }],
|
||||||
|
["@ai-sdk/xai", { reasoningEffort: "high" }],
|
||||||
|
["@ai-sdk/mistral", { reasoningEffort: "high" }],
|
||||||
|
["@ai-sdk/groq", { reasoningEffort: "high" }],
|
||||||
|
["@ai-sdk/cerebras", { reasoningEffort: "high" }],
|
||||||
|
["@ai-sdk/deepinfra", { reasoningEffort: "high" }],
|
||||||
|
["@ai-sdk/togetherai", { reasoningEffort: "high" }],
|
||||||
|
["venice-ai-sdk-provider", { reasoningEffort: "high" }],
|
||||||
|
["ai-gateway-provider", { reasoningEffort: "high" }],
|
||||||
|
["@ai-sdk/amazon-bedrock", { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }],
|
||||||
|
])("converts effort for %s", (npm, expected, ...args) => {
|
||||||
|
const id = args[0] as string | undefined
|
||||||
|
expect(ProviderTransform.reasoningVariants(model([{ type: "effort", values: ["high"] }]), target(npm, id))).toEqual(
|
||||||
|
{ high: expected },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses bare effort for Claude Opus 4.5", () => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(
|
||||||
|
model([{ type: "effort", values: ["high"] }]),
|
||||||
|
target("@ai-sdk/anthropic", "claude-opus-4-5"),
|
||||||
|
),
|
||||||
|
).toEqual({ high: { effort: "high" } })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("leaves legacy Anthropic effort options to budget fallback", () => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(
|
||||||
|
model([{ type: "effort", values: ["high"] }]),
|
||||||
|
target("@ai-sdk/anthropic", "claude-sonnet-4"),
|
||||||
|
),
|
||||||
|
).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses adaptive reasoning config for Anthropic models on Bedrock", () => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(
|
||||||
|
model([{ type: "effort", values: ["high"] }]),
|
||||||
|
target("@ai-sdk/amazon-bedrock", "anthropic.claude-opus-4-7-v1:0"),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
high: {
|
||||||
|
reasoningConfig: {
|
||||||
|
type: "adaptive",
|
||||||
|
maxReasoningEffort: "high",
|
||||||
|
display: "summarized",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("leaves legacy Anthropic Bedrock effort options to budget fallback", () => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(
|
||||||
|
model([{ type: "effort", values: ["high"] }]),
|
||||||
|
target("@ai-sdk/amazon-bedrock", "anthropic.claude-sonnet-4-v1:0"),
|
||||||
|
),
|
||||||
|
).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
["@openrouter/ai-sdk-provider", { reasoning: { max_tokens: 16_000 } }],
|
||||||
|
["@ai-sdk/anthropic", { thinking: { type: "enabled", budgetTokens: 16_000 } }],
|
||||||
|
["@ai-sdk/google-vertex/anthropic", { thinking: { type: "enabled", budgetTokens: 16_000 } }],
|
||||||
|
["@ai-sdk/google", { thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } }],
|
||||||
|
["@ai-sdk/google-vertex", { thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } }],
|
||||||
|
["@ai-sdk/amazon-bedrock", { reasoningConfig: { type: "enabled", budgetTokens: 16_000 } }],
|
||||||
|
["@ai-sdk/cohere", { thinking: { type: "enabled", tokenBudget: 16_000 } }],
|
||||||
|
["@ai-sdk/alibaba", { enableThinking: true, thinkingBudget: 16_000 }],
|
||||||
|
])("converts token budgets for %s", (npm, high) => {
|
||||||
|
const variants = ProviderTransform.reasoningVariants(model([{ type: "budget_tokens", min: 1_024 }]), target(npm))
|
||||||
|
expect(variants?.high).toEqual(high)
|
||||||
|
expect(Object.keys(variants ?? {})).toEqual(["high", "max"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("maps null effort to none", () => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(model([{ type: "effort", values: [null] }]), target("@ai-sdk/openai")),
|
||||||
|
).toEqual({
|
||||||
|
none: {
|
||||||
|
reasoningEffort: "none",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
include: ["reasoning.encrypted_content"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
["@ai-sdk/alibaba", { none: { enableThinking: false }, high: { enableThinking: true } }],
|
||||||
|
[
|
||||||
|
"@ai-sdk/cohere",
|
||||||
|
{
|
||||||
|
none: { thinking: { type: "disabled" } },
|
||||||
|
high: { thinking: { type: "enabled" } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])("converts toggle options for %s", (npm, expected) => {
|
||||||
|
expect(ProviderTransform.reasoningVariants(model([{ type: "toggle" }]), target(npm))).toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("combines Cohere toggle and budget options", () => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(
|
||||||
|
model([{ type: "toggle" }, { type: "budget_tokens", min: 1 }]),
|
||||||
|
target("@ai-sdk/cohere"),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
none: { thinking: { type: "disabled" } },
|
||||||
|
high: { thinking: { type: "enabled", tokenBudget: 16_000 } },
|
||||||
|
max: { thinking: { type: "enabled", tokenBudget: 31_999 } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("generates bounded high and max token budgets", () => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(
|
||||||
|
model([{ type: "budget_tokens", min: 1_024, max: 64_000 }]),
|
||||||
|
target("@ai-sdk/anthropic"),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
high: { thinking: { type: "enabled", budgetTokens: 16_000 } },
|
||||||
|
max: { thinking: { type: "enabled", budgetTokens: 31_999 } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("caps token budgets below the model output limit", () => {
|
||||||
|
const anthropic = target("@ai-sdk/anthropic")
|
||||||
|
anthropic.limit.output = 5_000
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(model([{ type: "budget_tokens", min: 1_024, max: 64_000 }]), anthropic),
|
||||||
|
).toEqual({
|
||||||
|
high: { thinking: { type: "enabled", budgetTokens: 2_500 } },
|
||||||
|
max: { thinking: { type: "enabled", budgetTokens: 4_999 } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("derives high and max budgets when models.dev omits max", () => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(
|
||||||
|
model([{ type: "budget_tokens", min: 1_024 }]),
|
||||||
|
target("@ai-sdk/anthropic", "claude-haiku-4-5"),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
high: { thinking: { type: "enabled", budgetTokens: 16_000 } },
|
||||||
|
max: { thinking: { type: "enabled", budgetTokens: 31_999 } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves explicit inclusive budget maxima", () => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(
|
||||||
|
model([{ type: "budget_tokens", min: 1_024, max: 24_576 }]),
|
||||||
|
target("@ai-sdk/google", "gemini-2.5-pro"),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 12_288 } },
|
||||||
|
max: { thinkingConfig: { includeThoughts: true, thinkingBudget: 24_576 } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("prefers effort options over token budgets", () => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(
|
||||||
|
model([
|
||||||
|
{ type: "budget_tokens", min: 1_024, max: 64_000 },
|
||||||
|
{ type: "effort", values: ["low"] },
|
||||||
|
]),
|
||||||
|
target("@ai-sdk/openai"),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
low: {
|
||||||
|
reasoningEffort: "low",
|
||||||
|
reasoningSummary: "auto",
|
||||||
|
include: ["reasoning.encrypted_content"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("leaves unsupported options for heuristic fallback", () => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(model([{ type: "effort", values: ["high"] }]), target("@ai-sdk/perplexity")),
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(ProviderTransform.reasoningVariants(model([{ type: "toggle" }]), target("@ai-sdk/openai"))).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses model-family options for gateway and GitHub Copilot", () => {
|
||||||
|
const effort = model([{ type: "effort", values: ["high"] }])
|
||||||
|
expect(ProviderTransform.reasoningVariants(effort, target("@ai-sdk/gateway", "anthropic/claude-sonnet-4"))).toEqual(
|
||||||
|
{
|
||||||
|
high: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(ProviderTransform.reasoningVariants(effort, target("@ai-sdk/gateway", "google/gemini-3-pro"))).toEqual({
|
||||||
|
high: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(effort, target("@ai-sdk/github-copilot", "gemini-3-pro")),
|
||||||
|
).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each(["@ai-sdk/cohere", "@ai-sdk/perplexity", "@ai-sdk/vercel", "@ai-sdk/alibaba", "gitlab-ai-provider"])(
|
||||||
|
"does not invent effort controls for %s",
|
||||||
|
(npm) => {
|
||||||
|
expect(
|
||||||
|
ProviderTransform.reasoningVariants(model([{ type: "effort", values: ["high"] }]), target(npm)),
|
||||||
|
).toBeUndefined()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
describe("ProviderTransform.variants", () => {
|
describe("ProviderTransform.variants", () => {
|
||||||
const createMockModel = (overrides: Partial<any> = {}): any => ({
|
const createMockModel = (overrides: Partial<any> = {}): any => ({
|
||||||
id: "test/test-model",
|
id: "test/test-model",
|
||||||
@@ -3372,6 +3672,42 @@ describe("ProviderTransform.variants", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("@ai-sdk/gateway", () => {
|
describe("@ai-sdk/gateway", () => {
|
||||||
|
test("configured anthropic aliases route by the API ID", () => {
|
||||||
|
const model = createMockModel({
|
||||||
|
id: "my-claude",
|
||||||
|
providerID: "gateway",
|
||||||
|
api: {
|
||||||
|
id: "anthropic/claude-sonnet-4-6",
|
||||||
|
url: "https://gateway.ai",
|
||||||
|
npm: "@ai-sdk/gateway",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const result = ProviderTransform.variants(model)
|
||||||
|
expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
|
||||||
|
expect(result.high).toEqual({
|
||||||
|
thinking: {
|
||||||
|
type: "adaptive",
|
||||||
|
},
|
||||||
|
effort: "high",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("configured google aliases route by the API ID", () => {
|
||||||
|
const model = createMockModel({
|
||||||
|
id: "my-gemini",
|
||||||
|
providerID: "gateway",
|
||||||
|
api: {
|
||||||
|
id: "google/gemini-2.5-pro",
|
||||||
|
url: "https://gateway.ai",
|
||||||
|
npm: "@ai-sdk/gateway",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(ProviderTransform.variants(model)).toEqual({
|
||||||
|
high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } },
|
||||||
|
max: { thinkingConfig: { includeThoughts: true, thinkingBudget: 32_768 } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("anthropic sonnet 4.6 models return adaptive thinking options", () => {
|
test("anthropic sonnet 4.6 models return adaptive thinking options", () => {
|
||||||
const model = createMockModel({
|
const model = createMockModel({
|
||||||
id: "anthropic/claude-sonnet-4-6",
|
id: "anthropic/claude-sonnet-4-6",
|
||||||
|
|||||||
@@ -1092,7 +1092,7 @@ describe("session.llm.stream", () => {
|
|||||||
const agent = {
|
const agent = {
|
||||||
name: "test",
|
name: "test",
|
||||||
mode: "primary",
|
mode: "primary",
|
||||||
options: {},
|
options: { reasoningMode: "pro" },
|
||||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
} satisfies Agent.Info
|
} satisfies Agent.Info
|
||||||
@@ -1123,6 +1123,7 @@ describe("session.llm.stream", () => {
|
|||||||
expect(body.model).toBe(resolved.api.id)
|
expect(body.model).toBe(resolved.api.id)
|
||||||
expect(body.stream).toBe(true)
|
expect(body.stream).toBe(true)
|
||||||
expect((body.reasoning as { effort?: string } | undefined)?.effort).toBe("high")
|
expect((body.reasoning as { effort?: string } | undefined)?.effort).toBe("high")
|
||||||
|
expect((body.reasoning as { mode?: string } | undefined)?.mode).toBe("pro")
|
||||||
|
|
||||||
const maxTokens = body.max_output_tokens as number | undefined
|
const maxTokens = body.max_output_tokens as number | undefined
|
||||||
expect(maxTokens).toBe(undefined) // match codex cli behavior
|
expect(maxTokens).toBe(undefined) // match codex cli behavior
|
||||||
|
|||||||
+165476
-110083
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { FilePart } from "@opencode-ai/sdk/v2"
|
import type { FilePart } from "@opencode-ai/sdk/v2"
|
||||||
import { attached, inline, kind } from "./message-file"
|
import { attached, inline, kind, typeLabel } from "./message-file"
|
||||||
|
|
||||||
function file(part: Partial<FilePart> = {}): FilePart {
|
function file(part: Partial<FilePart> = {}): FilePart {
|
||||||
return {
|
return {
|
||||||
@@ -52,4 +52,14 @@ describe("message-file", () => {
|
|||||||
expect(kind(file({ mime: "image/png" }))).toBe("image")
|
expect(kind(file({ mime: "image/png" }))).toBe("image")
|
||||||
expect(kind(file({ mime: "application/pdf" }))).toBe("file")
|
expect(kind(file({ mime: "application/pdf" }))).toBe("file")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("labels attachment types from the basename extension", () => {
|
||||||
|
expect(typeLabel("list.md", "text/plain")).toBe("Markdown")
|
||||||
|
expect(typeLabel("/repo/src/main.ts", "text/plain")).toBe("TypeScript")
|
||||||
|
expect(typeLabel("/tmp/report.pdf", "application/pdf")).toBe("PDF")
|
||||||
|
expect(typeLabel("notes.xyz", "text/plain")).toBe("XYZ")
|
||||||
|
expect(typeLabel("/home/user/my.project/Makefile", "text/plain")).toBe("File")
|
||||||
|
expect(typeLabel(".gitignore", "text/plain")).toBe("File")
|
||||||
|
expect(typeLabel("/repo/.env", "text/plain")).toBe("File")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { bundledLanguagesInfo } from "shiki"
|
||||||
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import type { FilePart } from "@opencode-ai/sdk/v2"
|
import type { FilePart } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
export function attached(part: FilePart) {
|
export function attached(part: FilePart) {
|
||||||
@@ -12,3 +14,22 @@ export function inline(part: FilePart) {
|
|||||||
export function kind(part: FilePart) {
|
export function kind(part: FilePart) {
|
||||||
return part.mime.startsWith("image/") ? "image" : "file"
|
return part.mime.startsWith("image/") ? "image" : "file"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// language metadata only; grammars stay behind shiki's lazy imports
|
||||||
|
const LANGUAGE_NAMES = new Map<string, string>(
|
||||||
|
bundledLanguagesInfo.flatMap((info) =>
|
||||||
|
[info.id, ...(info.aliases ?? [])].map((alias) => [alias, info.name] as [string, string]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// attachments carry text/plain for all text files, so the label comes from the extension;
|
||||||
|
// filename may be an absolute path, so extract the basename before looking for one
|
||||||
|
export function typeLabel(filename: string, mime: string) {
|
||||||
|
if (mime === "application/pdf") return "PDF"
|
||||||
|
const base = getFilename(filename)
|
||||||
|
// idx 0 is a dotfile like .gitignore, not an extension
|
||||||
|
const idx = base.lastIndexOf(".")
|
||||||
|
const suffix = idx <= 0 ? "" : base.slice(idx + 1).toLowerCase()
|
||||||
|
if (!suffix) return "File"
|
||||||
|
return LANGUAGE_NAMES.get(suffix) ?? suffix.toUpperCase()
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,8 +57,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-type="image"] {
|
&[data-type="image"] {
|
||||||
width: 48px;
|
position: relative;
|
||||||
height: 48px;
|
width: 58px;
|
||||||
|
height: 46px;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
/* inset box-shadows do not paint over <img> content, so the hairline is an overlay */
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-base);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-type="file"] {
|
&[data-type="file"] {
|
||||||
@@ -1323,6 +1335,16 @@ body:not([data-new-layout]) {
|
|||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--border-strong-base);
|
border-color: var(--border-strong-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-type="image"] {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 1px solid var(--border-weak-base);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="user-message-attachment-name"] {
|
[data-slot="user-message-attachment-name"] {
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
|||||||
import { Markdown } from "./markdown"
|
import { Markdown } from "./markdown"
|
||||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||||
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||||
|
import { AttachmentCardV2 } from "../v2/components/attachment-card-v2"
|
||||||
|
import { CommentCardV2 } from "../v2/components/comment-card-v2"
|
||||||
import { checksum } from "@opencode-ai/core/util/encode"
|
import { checksum } from "@opencode-ai/core/util/encode"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
@@ -60,7 +62,7 @@ import { ToolStatusTitle } from "./tool-status-title"
|
|||||||
import { patchFiles } from "./apply-patch-file"
|
import { patchFiles } from "./apply-patch-file"
|
||||||
import { animate } from "motion"
|
import { animate } from "motion"
|
||||||
import { useLocation } from "@solidjs/router"
|
import { useLocation } from "@solidjs/router"
|
||||||
import { attached, inline, kind } from "./message-file"
|
import { attached, inline, kind, typeLabel } from "./message-file"
|
||||||
import { readPartText } from "./message-part-text"
|
import { readPartText } from "./message-part-text"
|
||||||
import { SessionProgressIndicatorV2 } from "../v2/components/session-progress-indicator-v2"
|
import { SessionProgressIndicatorV2 } from "../v2/components/session-progress-indicator-v2"
|
||||||
|
|
||||||
@@ -167,6 +169,7 @@ export interface MessageProps {
|
|||||||
showAssistantCopyPartID?: string | null
|
showAssistantCopyPartID?: string | null
|
||||||
showReasoningSummaries?: boolean
|
showReasoningSummaries?: boolean
|
||||||
useV2Actions?: boolean
|
useV2Actions?: boolean
|
||||||
|
comments?: UserMessageComment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionAction = (input: { sessionID: string; messageID: string }) => Promise<void> | void
|
export type SessionAction = (input: { sessionID: string; messageID: string }) => Promise<void> | void
|
||||||
@@ -174,6 +177,16 @@ export type SessionAction = (input: { sessionID: string; messageID: string }) =>
|
|||||||
export type UserActions = {
|
export type UserActions = {
|
||||||
fork?: SessionAction
|
fork?: SessionAction
|
||||||
revert?: SessionAction
|
revert?: SessionAction
|
||||||
|
openAttachment?: (file: FilePart) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserMessageComment = {
|
||||||
|
path: string
|
||||||
|
comment: string
|
||||||
|
selection?: {
|
||||||
|
startLine: number
|
||||||
|
endLine: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessagePartProps {
|
export interface MessagePartProps {
|
||||||
@@ -946,6 +959,7 @@ export function Message(props: MessageProps) {
|
|||||||
parts={props.parts}
|
parts={props.parts}
|
||||||
actions={props.actions}
|
actions={props.actions}
|
||||||
useV2Actions={props.useV2Actions}
|
useV2Actions={props.useV2Actions}
|
||||||
|
comments={props.comments}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Match>
|
</Match>
|
||||||
@@ -1163,6 +1177,7 @@ export function UserMessageDisplay(props: {
|
|||||||
parts: PartType[]
|
parts: PartType[]
|
||||||
actions?: UserActions
|
actions?: UserActions
|
||||||
useV2Actions?: boolean
|
useV2Actions?: boolean
|
||||||
|
comments?: UserMessageComment[]
|
||||||
}) {
|
}) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
@@ -1184,6 +1199,8 @@ export function UserMessageDisplay(props: {
|
|||||||
|
|
||||||
const attachments = createMemo(() => files().filter(attached))
|
const attachments = createMemo(() => files().filter(attached))
|
||||||
|
|
||||||
|
const messageComments = createMemo(() => (newLayout() ? (props.comments ?? []) : []))
|
||||||
|
|
||||||
const inlineFiles = createMemo(() => files().filter(inline))
|
const inlineFiles = createMemo(() => files().filter(inline))
|
||||||
|
|
||||||
const agents = createMemo(() => (props.parts?.filter((p) => p.type === "agent") as AgentPart[]) ?? [])
|
const agents = createMemo(() => (props.parts?.filter((p) => p.type === "agent") as AgentPart[]) ?? [])
|
||||||
@@ -1240,14 +1257,27 @@ export function UserMessageDisplay(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component="user-message" data-timeline-part-id={textPart()?.id}>
|
<div data-component="user-message" data-timeline-part-id={textPart()?.id}>
|
||||||
<Show when={attachments().length > 0}>
|
<Show when={attachments().length > 0 || messageComments().length > 0}>
|
||||||
<div data-slot="user-message-attachments">
|
<div data-slot="user-message-attachments">
|
||||||
|
<For each={messageComments()}>
|
||||||
|
{(comment) => (
|
||||||
|
<CommentCardV2
|
||||||
|
comment={comment.comment}
|
||||||
|
path={comment.path}
|
||||||
|
selection={comment.selection}
|
||||||
|
title={comment.comment}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
<For each={attachments()}>
|
<For each={attachments()}>
|
||||||
{(file) => {
|
{(file) => {
|
||||||
const type = kind(file)
|
const type = kind(file)
|
||||||
const name = file.filename ?? i18n.t("ui.message.attachment.alt")
|
const name = file.filename ?? i18n.t("ui.message.attachment.alt")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Show
|
||||||
|
when={newLayout() && type === "file"}
|
||||||
|
fallback={
|
||||||
<div
|
<div
|
||||||
data-slot="user-message-attachment"
|
data-slot="user-message-attachment"
|
||||||
data-type={type}
|
data-type={type}
|
||||||
@@ -1269,6 +1299,17 @@ export function UserMessageDisplay(props: {
|
|||||||
<img data-slot="user-message-attachment-image" src={file.url} alt={name} />
|
<img data-slot="user-message-attachment-image" src={file.url} alt={name} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AttachmentCardV2
|
||||||
|
title={getFilename(name)}
|
||||||
|
hover={name}
|
||||||
|
clickable={!!props.actions?.openAttachment}
|
||||||
|
onClick={() => props.actions?.openAttachment?.(file)}
|
||||||
|
>
|
||||||
|
{typeLabel(name, file.mime)}
|
||||||
|
</AttachmentCardV2>
|
||||||
|
</Show>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
[data-component="attachment-card-v2"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 160px;
|
||||||
|
min-width: 160px;
|
||||||
|
max-width: 160px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--v2-overlay-simple-overlay-hover);
|
||||||
|
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-base);
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
&[data-active] {
|
||||||
|
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-clickable] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="attachment-card-v2-title"],
|
||||||
|
[data-slot="attachment-card-v2-subtitle"] {
|
||||||
|
max-width: 100%;
|
||||||
|
font-family: var(--v2-font-family-sans, "Inter", sans-serif);
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 12px;
|
||||||
|
letter-spacing: 0.05px;
|
||||||
|
font-variation-settings: "slnt" 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="attachment-card-v2-title"] {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 530;
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="attachment-card-v2-subtitle"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 440;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
|
||||||
|
[data-component="file-icon"] {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { JSX } from "solid-js"
|
||||||
|
import "./attachment-card-v2.css"
|
||||||
|
|
||||||
|
/** Shared 160px two-line card used by v2 file and comment attachments in the composer and timeline. */
|
||||||
|
export function AttachmentCardV2(props: {
|
||||||
|
title: string
|
||||||
|
active?: boolean
|
||||||
|
clickable?: boolean
|
||||||
|
/** native title attribute */
|
||||||
|
hover?: string
|
||||||
|
onClick?: () => void
|
||||||
|
children: JSX.Element
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-component="attachment-card-v2"
|
||||||
|
data-active={props.active ? "true" : undefined}
|
||||||
|
data-clickable={props.clickable ? "true" : undefined}
|
||||||
|
title={props.hover}
|
||||||
|
onClick={() => props.onClick?.()}
|
||||||
|
>
|
||||||
|
<span data-slot="attachment-card-v2-title">{props.title}</span>
|
||||||
|
<span data-slot="attachment-card-v2-subtitle">{props.children}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Show } from "solid-js"
|
||||||
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
|
import { getFilenameTruncated } from "@opencode-ai/core/util/path"
|
||||||
|
import { AttachmentCardV2 } from "./attachment-card-v2"
|
||||||
|
|
||||||
|
export function CommentCardV2(props: {
|
||||||
|
comment: string
|
||||||
|
path: string
|
||||||
|
selection?: { startLine: number; endLine: number }
|
||||||
|
active?: boolean
|
||||||
|
title?: string
|
||||||
|
onClick?: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AttachmentCardV2 title={props.comment} active={props.active} hover={props.title} onClick={props.onClick}>
|
||||||
|
<FileIcon node={{ path: props.path, type: "file" }} />
|
||||||
|
<span>
|
||||||
|
{getFilenameTruncated(props.path, 14)}
|
||||||
|
<Show when={props.selection}>
|
||||||
|
{(sel) =>
|
||||||
|
sel().startLine === sel().endLine ? `:${sel().startLine}` : `:${sel().startLine}-${sel().endLine}`
|
||||||
|
}
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</AttachmentCardV2>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { useI18n } from "@opencode-ai/ui/context/i18n"
|
import { useI18n } from "@opencode-ai/ui/context/i18n"
|
||||||
import { cloneSelectedLineRange, formatSelectedLineLabel } from "../../pierre/selection-bridge"
|
import { cloneSelectedLineRange, formatSelectedLineLabel } from "../../pierre/selection-bridge"
|
||||||
import { LineCommentEditorV2, LineCommentV2 } from "@opencode-ai/ui/v2/line-comment-v2"
|
import { LineCommentEditorV2, LineCommentV2 } from "@opencode-ai/ui/v2/line-comment-v2"
|
||||||
|
import type { LineCommentEditorV2Mention } from "@opencode-ai/ui/v2/line-comment-v2"
|
||||||
|
|
||||||
type LineCommentControllerV2Props<T extends LineCommentShape> = {
|
type LineCommentControllerV2Props<T extends LineCommentShape> = {
|
||||||
comments: Accessor<T[]>
|
comments: Accessor<T[]>
|
||||||
@@ -23,6 +24,7 @@ type LineCommentControllerV2Props<T extends LineCommentShape> = {
|
|||||||
onDelete?: (comment: T) => void
|
onDelete?: (comment: T) => void
|
||||||
renderCommentActions?: (comment: T, controls: { edit: VoidFunction; remove: VoidFunction }) => JSX.Element
|
renderCommentActions?: (comment: T, controls: { edit: VoidFunction; remove: VoidFunction }) => JSX.Element
|
||||||
editSubmitLabel?: string
|
editSubmitLabel?: string
|
||||||
|
mention?: LineCommentEditorV2Mention
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommentProps = {
|
type CommentProps = {
|
||||||
@@ -43,6 +45,7 @@ type DraftProps = {
|
|||||||
onSubmit: (value: string) => void
|
onSubmit: (value: string) => void
|
||||||
cancelLabel?: string
|
cancelLabel?: string
|
||||||
submitLabel?: string
|
submitLabel?: string
|
||||||
|
mention?: LineCommentEditorV2Mention
|
||||||
}
|
}
|
||||||
|
|
||||||
function lineCommentElementV2(view: Accessor<CommentProps>) {
|
function lineCommentElementV2(view: Accessor<CommentProps>) {
|
||||||
@@ -70,6 +73,7 @@ function lineCommentElementV2(view: Accessor<CommentProps>) {
|
|||||||
onSubmit={view().editor!.onSubmit}
|
onSubmit={view().editor!.onSubmit}
|
||||||
cancelLabel={view().editor!.cancelLabel}
|
cancelLabel={view().editor!.cancelLabel}
|
||||||
submitLabel={view().editor!.submitLabel}
|
submitLabel={view().editor!.submitLabel}
|
||||||
|
mention={view().editor!.mention}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -87,6 +91,7 @@ function lineCommentDraftElementV2(view: Accessor<DraftProps>) {
|
|||||||
onSubmit={view().onSubmit}
|
onSubmit={view().onSubmit}
|
||||||
cancelLabel={view().cancelLabel}
|
cancelLabel={view().cancelLabel}
|
||||||
submitLabel={view().submitLabel}
|
submitLabel={view().submitLabel}
|
||||||
|
mention={view().mention}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -142,6 +147,7 @@ export function createLineCommentControllerV2<T extends LineCommentShape>(props:
|
|||||||
},
|
},
|
||||||
cancelLabel: i18n.t("ui.lineComment.cancel"),
|
cancelLabel: i18n.t("ui.lineComment.cancel"),
|
||||||
submitLabel: props.editSubmitLabel,
|
submitLabel: props.editSubmitLabel,
|
||||||
|
mention: props.mention,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
},
|
},
|
||||||
@@ -165,6 +171,7 @@ export function createLineCommentControllerV2<T extends LineCommentShape>(props:
|
|||||||
},
|
},
|
||||||
cancelLabel: i18n.t("ui.lineComment.cancel"),
|
cancelLabel: i18n.t("ui.lineComment.cancel"),
|
||||||
submitLabel: i18n.t("ui.lineComment.submit"),
|
submitLabel: i18n.t("ui.lineComment.submit"),
|
||||||
|
mention: props.mention,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -202,6 +209,7 @@ export function createLineCommentControllerV2<T extends LineCommentShape>(props:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
note,
|
||||||
annotations,
|
annotations,
|
||||||
renderAnnotation,
|
renderAnnotation,
|
||||||
renderGutterUtility,
|
renderGutterUtility,
|
||||||
|
|||||||
@@ -76,6 +76,11 @@
|
|||||||
color: var(--text-strong, var(--v2-text-text-base));
|
color: var(--text-strong, var(--v2-text-text-base));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-component="session-review-v2-sidebar-root"]
|
||||||
|
[data-slot="session-review-v2-sidebar-title"]:not(:has([data-component="select-v2-root"])) {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
[data-component="session-review-v2-sidebar-root"]
|
[data-component="session-review-v2-sidebar-root"]
|
||||||
[data-slot="session-review-v2-sidebar-title"]
|
[data-slot="session-review-v2-sidebar-title"]
|
||||||
[data-component="select-v2-root"] {
|
[data-component="select-v2-root"] {
|
||||||
@@ -97,7 +102,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin-top: 2px;
|
margin-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] {
|
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export type SessionReviewV2Props = {
|
|||||||
empty?: JSX.Element
|
empty?: JSX.Element
|
||||||
sidebarOpen?: boolean
|
sidebarOpen?: boolean
|
||||||
sidebar?: JSX.Element
|
sidebar?: JSX.Element
|
||||||
sidebarToggle?: JSX.Element
|
|
||||||
activeFile?: string
|
activeFile?: string
|
||||||
files: string[]
|
files: string[]
|
||||||
onSelectFile: (file: string) => void
|
onSelectFile: (file: string) => void
|
||||||
@@ -199,7 +198,6 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
|||||||
|
|
||||||
const toolbarStart = () => (
|
const toolbarStart = () => (
|
||||||
<>
|
<>
|
||||||
{props.sidebarToggle}
|
|
||||||
<Show when={showCollapsedMeta()}>
|
<Show when={showCollapsedMeta()}>
|
||||||
<div data-slot="session-review-v2-toolbar-collapsed-meta">
|
<div data-slot="session-review-v2-toolbar-collapsed-meta">
|
||||||
<Show when={title()}>
|
<Show when={title()}>
|
||||||
@@ -319,7 +317,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SessionReviewV2SidebarToggle(props: { opened: boolean; onToggle: () => void }) {
|
export function SessionReviewV2SidebarToggle(props: { opened: boolean; disabled?: boolean; onToggle: () => void }) {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -330,6 +328,7 @@ export function SessionReviewV2SidebarToggle(props: { opened: boolean; onToggle:
|
|||||||
class="session-review-v2-sidebar-toggle"
|
class="session-review-v2-sidebar-toggle"
|
||||||
aria-label={i18n.t("ui.sessionReviewV2.toggleSidebar")}
|
aria-label={i18n.t("ui.sessionReviewV2.toggleSidebar")}
|
||||||
aria-expanded={props.opened}
|
aria-expanded={props.opened}
|
||||||
|
disabled={props.disabled}
|
||||||
onClick={props.onToggle}
|
onClick={props.onToggle}
|
||||||
icon={<Icon name="filetree" />}
|
icon={<Icon name="filetree" />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/stats-core": "workspace:*",
|
"@opencode-ai/stats-core": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
uniqueComparisonPairs,
|
uniqueComparisonPairs,
|
||||||
type ComparisonModelRef,
|
type ComparisonModelRef,
|
||||||
} from "../compare-cards"
|
} from "../compare-cards"
|
||||||
|
import { BreadcrumbSelect } from "../breadcrumb-select"
|
||||||
import {
|
import {
|
||||||
applyThemePreference,
|
applyThemePreference,
|
||||||
Footer,
|
Footer,
|
||||||
@@ -294,61 +295,46 @@ function ModelHero(props: {
|
|||||||
<Show
|
<Show
|
||||||
when={labs().length > 0}
|
when={labs().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<span data-slot="model-hero-crumb" data-current="true">
|
<span data-slot="model-hero-crumb" data-menu="true">
|
||||||
<span>{props.labName}</span>
|
<span>{props.labName}</span>
|
||||||
<ChevronDownIcon />
|
<ChevronDownIcon />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<details data-component="model-hero-menu">
|
<BreadcrumbSelect
|
||||||
<summary data-slot="model-hero-crumb" data-current="true">
|
ariaLabel="Choose a lab"
|
||||||
<span>{props.labName}</span>
|
label={props.labName}
|
||||||
<ChevronDownIcon />
|
options={labs().map((lab) => ({
|
||||||
</summary>
|
href: language.route(`${import.meta.env.BASE_URL}${lab.id}`),
|
||||||
<div data-slot="model-hero-options">
|
label: lab.name,
|
||||||
<For each={labs()}>
|
value: lab.id,
|
||||||
{(lab) => (
|
}))}
|
||||||
<a
|
value={providerSlug(labId())}
|
||||||
data-slot="model-hero-option"
|
variant="model"
|
||||||
data-current={lab.id === providerSlug(labId()) ? "true" : undefined}
|
/>
|
||||||
href={language.route(`${import.meta.env.BASE_URL}${lab.id}`)}
|
|
||||||
>
|
|
||||||
{lab.name}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</Show>
|
</Show>
|
||||||
<span data-slot="model-hero-separator">/</span>
|
<span data-slot="model-hero-separator">/</span>
|
||||||
<Show
|
<Show
|
||||||
when={labModels().length > 0}
|
when={labModels().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<span data-slot="model-hero-crumb" data-current="true" aria-current="page">
|
<span data-slot="model-hero-crumb" data-menu="true" data-current="true" aria-current="page">
|
||||||
<span>{modelName()}</span>
|
<span>{modelName()}</span>
|
||||||
<ChevronDownIcon />
|
<ChevronDownIcon />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<details data-component="model-hero-menu">
|
<BreadcrumbSelect
|
||||||
<summary data-slot="model-hero-crumb" data-current="true" aria-current="page">
|
ariaLabel="Choose a model"
|
||||||
<span>{modelName()}</span>
|
current
|
||||||
<ChevronDownIcon />
|
label={modelName()}
|
||||||
</summary>
|
options={labModels().map((model) => ({
|
||||||
<div data-slot="model-hero-options">
|
href: language.route(`${import.meta.env.BASE_URL}${model.id}`),
|
||||||
<For each={labModels()}>
|
label: model.name,
|
||||||
{(model) => (
|
value: model.id,
|
||||||
<a
|
}))}
|
||||||
data-slot="model-hero-option"
|
value={props.catalog?.id ?? ""}
|
||||||
data-current={model.id === props.catalog?.id ? "true" : undefined}
|
variant="model"
|
||||||
href={language.route(`${import.meta.env.BASE_URL}${model.id}`)}
|
/>
|
||||||
>
|
|
||||||
{model.name}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</Show>
|
</Show>
|
||||||
</nav>
|
</nav>
|
||||||
<div data-slot="model-hero-title-row">
|
<div data-slot="model-hero-title-row">
|
||||||
@@ -432,7 +418,7 @@ function ModelHeroSparkline(props: { data: StatsModelData }) {
|
|||||||
function ChevronDownIcon() {
|
function ChevronDownIcon() {
|
||||||
return (
|
return (
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" fill="none">
|
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" fill="none">
|
||||||
<path d="M4.75 6.25L8 9.5L11.25 6.25" stroke="currentColor" stroke-width="1.5" />
|
<path d="M5 6.5L8 9.5L11 6.5" stroke="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { SectionHeading } from "../section-heading"
|
|||||||
import { runStatsEffect } from "../../stats-runtime"
|
import { runStatsEffect } from "../../stats-runtime"
|
||||||
import { setStatsPageCacheHeaders } from "../stats-cache"
|
import { setStatsPageCacheHeaders } from "../stats-cache"
|
||||||
import { ComparisonCardsSection, modelRefFromCatalog, uniqueComparisonPairs } from "../compare-cards"
|
import { ComparisonCardsSection, modelRefFromCatalog, uniqueComparisonPairs } from "../compare-cards"
|
||||||
|
import { BreadcrumbSelect } from "../breadcrumb-select"
|
||||||
import {
|
import {
|
||||||
applyThemePreference,
|
applyThemePreference,
|
||||||
Footer,
|
Footer,
|
||||||
@@ -205,6 +206,7 @@ function LabHero(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[] }) {
|
|||||||
function LabHeroBreadcrumb(props: { label: string; labs?: ModelCatalogLab[] }) {
|
function LabHeroBreadcrumb(props: { label: string; labs?: ModelCatalogLab[] }) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const labs = () => props.labs ?? []
|
const labs = () => props.labs ?? []
|
||||||
|
const current = () => labs().find((lab) => lab.name === props.label)
|
||||||
return (
|
return (
|
||||||
<nav data-component="lab-hero-breadcrumb" aria-label="Data breadcrumb">
|
<nav data-component="lab-hero-breadcrumb" aria-label="Data breadcrumb">
|
||||||
<a data-slot="lab-hero-crumb" href={language.route(import.meta.env.BASE_URL)}>
|
<a data-slot="lab-hero-crumb" href={language.route(import.meta.env.BASE_URL)}>
|
||||||
@@ -217,32 +219,23 @@ function LabHeroBreadcrumb(props: { label: string; labs?: ModelCatalogLab[] }) {
|
|||||||
<span data-slot="lab-hero-crumb" data-current="true" aria-current="page">
|
<span data-slot="lab-hero-crumb" data-current="true" aria-current="page">
|
||||||
<span>{props.label}</span>
|
<span>{props.label}</span>
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
|
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
<path d="M4.75 6.25L8 9.5L11.25 6.25" fill="none" stroke="currentColor" stroke-width="1.5" />
|
<path d="M5 6.5L8 9.5L11 6.5" fill="none" stroke="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<details data-component="lab-hero-menu">
|
<BreadcrumbSelect
|
||||||
<summary data-slot="lab-hero-crumb" data-current="true" aria-current="page">
|
ariaLabel="Choose a lab"
|
||||||
<span>{props.label}</span>
|
current
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
|
label={props.label}
|
||||||
<path d="M4.75 6.25L8 9.5L11.25 6.25" fill="none" stroke="currentColor" stroke-width="1.5" />
|
options={labs().map((lab) => ({
|
||||||
</svg>
|
href: language.route(`${import.meta.env.BASE_URL}${lab.id}`),
|
||||||
</summary>
|
label: lab.name,
|
||||||
<div data-slot="lab-hero-options">
|
value: lab.id,
|
||||||
<For each={labs()}>
|
}))}
|
||||||
{(lab) => (
|
value={current()?.id ?? ""}
|
||||||
<a
|
variant="lab"
|
||||||
data-slot="lab-hero-option"
|
/>
|
||||||
data-current={lab.name === props.label ? "true" : undefined}
|
|
||||||
href={language.route(`${import.meta.env.BASE_URL}${lab.id}`)}
|
|
||||||
>
|
|
||||||
{lab.name}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</Show>
|
</Show>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Select } from "@kobalte/core/select"
|
||||||
|
import { createMemo } from "solid-js"
|
||||||
|
|
||||||
|
export type BreadcrumbSelectOption = {
|
||||||
|
href: string
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BreadcrumbSelect(props: {
|
||||||
|
ariaLabel: string
|
||||||
|
current?: boolean
|
||||||
|
label: string
|
||||||
|
options: BreadcrumbSelectOption[]
|
||||||
|
value: string
|
||||||
|
variant: "lab" | "model"
|
||||||
|
}) {
|
||||||
|
const selected = createMemo(() => props.options.find((option) => option.value === props.value) ?? null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select<BreadcrumbSelectOption>
|
||||||
|
data-component="stats-breadcrumb-select"
|
||||||
|
data-variant={props.variant}
|
||||||
|
options={props.options}
|
||||||
|
optionValue="value"
|
||||||
|
optionTextValue="label"
|
||||||
|
value={selected()}
|
||||||
|
placeholder={props.label}
|
||||||
|
disallowEmptySelection
|
||||||
|
closeOnSelection
|
||||||
|
shouldFocusWrap
|
||||||
|
placement="bottom"
|
||||||
|
gutter={8}
|
||||||
|
sameWidth={false}
|
||||||
|
itemComponent={(itemProps) => (
|
||||||
|
<Select.Item
|
||||||
|
item={itemProps.item}
|
||||||
|
data-slot="stats-breadcrumb-select-option"
|
||||||
|
data-current={itemProps.item.rawValue.value === props.value ? "true" : undefined}
|
||||||
|
>
|
||||||
|
<Select.ItemLabel>{itemProps.item.rawValue.label}</Select.ItemLabel>
|
||||||
|
</Select.Item>
|
||||||
|
)}
|
||||||
|
onChange={(option) => {
|
||||||
|
if (!option || option.value === props.value) return
|
||||||
|
window.location.assign(option.href)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Trigger
|
||||||
|
data-slot="stats-breadcrumb-select-trigger"
|
||||||
|
data-current={props.current ? "true" : undefined}
|
||||||
|
data-variant={props.variant}
|
||||||
|
aria-label={props.ariaLabel}
|
||||||
|
aria-current={props.current ? "page" : undefined}
|
||||||
|
>
|
||||||
|
<Select.Value<BreadcrumbSelectOption> data-slot="stats-breadcrumb-select-value">
|
||||||
|
{(state) => <span>{state.selectedOption()?.label ?? props.label}</span>}
|
||||||
|
</Select.Value>
|
||||||
|
<Select.Icon data-slot="stats-breadcrumb-select-icon">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path d="M5 6.5L8 9.5L11 6.5" fill="none" stroke="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</Select.Icon>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.HiddenSelect />
|
||||||
|
<Select.Content data-component="stats-breadcrumb-select-content" data-variant={props.variant}>
|
||||||
|
<Select.Listbox data-slot="stats-breadcrumb-select-listbox" />
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -134,7 +134,7 @@ export default function ModelCompareIndex() {
|
|||||||
Data
|
Data
|
||||||
</a>
|
</a>
|
||||||
<span data-slot="compare-home-separator">/</span>
|
<span data-slot="compare-home-separator">/</span>
|
||||||
<span data-slot="compare-home-crumb" data-current="true">
|
<span data-slot="compare-home-crumb" data-current="true" aria-current="page">
|
||||||
Compare
|
Compare
|
||||||
</span>
|
</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -2888,7 +2888,7 @@
|
|||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--stats-layer-2);
|
background: var(--stats-layer-2);
|
||||||
color: var(--stats-faint);
|
color: var(--stats-hero-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
@@ -2897,21 +2897,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] a[data-slot="lab-hero-crumb"] {
|
[data-page="stats"] a[data-slot="lab-hero-crumb"] {
|
||||||
color: var(--stats-faint);
|
color: var(--stats-hero-muted);
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="lab-hero-menu"] {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="lab-hero-menu"] summary {
|
|
||||||
cursor: pointer;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="lab-hero-menu"] summary::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="lab-hero-crumb"][data-current="true"] {
|
[data-page="stats"] [data-slot="lab-hero-crumb"][data-current="true"] {
|
||||||
@@ -2920,9 +2906,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] a[data-slot="lab-hero-crumb"]:hover,
|
[data-page="stats"] a[data-slot="lab-hero-crumb"]:hover,
|
||||||
[data-page="stats"] a[data-slot="lab-hero-crumb"]:focus-visible,
|
[data-page="stats"] a[data-slot="lab-hero-crumb"]:focus-visible {
|
||||||
[data-page="stats"] summary[data-slot="lab-hero-crumb"]:hover,
|
|
||||||
[data-page="stats"] summary[data-slot="lab-hero-crumb"]:focus-visible {
|
|
||||||
color: var(--stats-text);
|
color: var(--stats-text);
|
||||||
outline: none;
|
outline: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -2941,57 +2925,6 @@
|
|||||||
color: currentColor;
|
color: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-component="lab-hero-menu"][open] [data-slot="lab-hero-crumb"] svg {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="lab-hero-options"] {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(100% + 8px);
|
|
||||||
left: 50%;
|
|
||||||
z-index: 5;
|
|
||||||
display: grid;
|
|
||||||
width: max-content;
|
|
||||||
min-width: 180px;
|
|
||||||
max-width: min(280px, calc(100vw - 48px));
|
|
||||||
max-height: min(360px, calc(100vh - 160px));
|
|
||||||
padding: 6px;
|
|
||||||
overflow: auto;
|
|
||||||
border: 1px solid var(--stats-line);
|
|
||||||
background: var(--stats-bg);
|
|
||||||
box-shadow: 0 12px 32px #0000001a;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="lab-hero-option"] {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
height: 30px;
|
|
||||||
padding: 0 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--stats-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.2;
|
|
||||||
text-decoration: none;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="lab-hero-option"]:hover,
|
|
||||||
[data-page="stats"] [data-slot="lab-hero-option"]:focus-visible {
|
|
||||||
background: var(--stats-layer-2);
|
|
||||||
color: var(--stats-text);
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="lab-hero-option"][data-current="true"] {
|
|
||||||
background: var(--stats-layer-2);
|
|
||||||
color: var(--stats-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="lab-hero-separator"] {
|
[data-page="stats"] [data-slot="lab-hero-separator"] {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -3113,29 +3046,147 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-hero-crumb"][data-current="true"] {
|
[data-page="stats"] a[data-slot="model-hero-crumb"] {
|
||||||
|
color: var(--stats-hero-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="model-hero-crumb"][data-menu="true"] {
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-hero-menu"] {
|
[data-page="stats"] [data-slot="model-hero-crumb"][data-current="true"] {
|
||||||
position: relative;
|
color: var(--stats-text);
|
||||||
display: inline-block;
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-component="stats-breadcrumb-select"] {
|
||||||
|
display: inline-flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-hero-menu"] summary {
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-trigger"] {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
max-width: min(100%, 280px);
|
||||||
|
height: 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 4px 0 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
appearance: none;
|
||||||
|
background: var(--stats-layer-2);
|
||||||
|
color: var(--stats-hero-muted);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.1;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
list-style: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-hero-menu"] summary::-webkit-details-marker {
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-trigger"][data-current="true"],
|
||||||
display: none;
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-trigger"]:hover,
|
||||||
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-trigger"][data-expanded] {
|
||||||
|
color: var(--stats-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-trigger"]:focus-visible {
|
||||||
|
color: var(--stats-text);
|
||||||
|
outline: 1px solid var(--stats-text);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-value"] {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-value"] span {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-icon"] {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--stats-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-icon"] svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"]
|
||||||
|
[data-slot="stats-breadcrumb-select-trigger"][data-expanded]
|
||||||
|
[data-slot="stats-breadcrumb-select-icon"] {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-popper-positioner]:has([data-component="stats-breadcrumb-select-content"]) {
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-component="stats-breadcrumb-select-content"] {
|
||||||
|
z-index: 20;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 180px;
|
||||||
|
max-width: min(360px, calc(100vw - 48px));
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--stats-line);
|
||||||
|
background: var(--stats-bg);
|
||||||
|
box-shadow: 0 12px 32px #0000001a;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-component="stats-breadcrumb-select-content"][data-variant="lab"] {
|
||||||
|
max-width: min(280px, calc(100vw - 48px));
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-listbox"] {
|
||||||
|
display: grid;
|
||||||
|
max-height: min(360px, calc(100vh - 160px));
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px;
|
||||||
|
overflow: auto;
|
||||||
|
list-style: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-option"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
height: 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--stats-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-option"][data-highlighted],
|
||||||
|
[data-page="stats"] [data-slot="stats-breadcrumb-select-option"][data-selected] {
|
||||||
|
background: var(--stats-layer-2);
|
||||||
|
color: var(--stats-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] a[data-slot="model-hero-crumb"]:hover,
|
[data-page="stats"] a[data-slot="model-hero-crumb"]:hover,
|
||||||
[data-page="stats"] a[data-slot="model-hero-crumb"]:focus-visible,
|
[data-page="stats"] a[data-slot="model-hero-crumb"]:focus-visible {
|
||||||
[data-page="stats"] summary[data-slot="model-hero-crumb"]:hover,
|
|
||||||
[data-page="stats"] summary[data-slot="model-hero-crumb"]:focus-visible {
|
|
||||||
color: var(--stats-text);
|
color: var(--stats-text);
|
||||||
outline: none;
|
outline: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -3151,58 +3202,7 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
color: var(--stats-faint);
|
color: currentColor;
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-hero-menu"][open] [data-slot="model-hero-crumb"] svg {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-hero-options"] {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(100% + 8px);
|
|
||||||
left: 50%;
|
|
||||||
z-index: 5;
|
|
||||||
display: grid;
|
|
||||||
width: max-content;
|
|
||||||
min-width: 180px;
|
|
||||||
max-width: min(360px, calc(100vw - 48px));
|
|
||||||
max-height: min(360px, calc(100vh - 160px));
|
|
||||||
padding: 6px;
|
|
||||||
overflow: auto;
|
|
||||||
border: 1px solid var(--stats-line);
|
|
||||||
background: var(--stats-bg);
|
|
||||||
box-shadow: 0 12px 32px #0000001a;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-hero-option"] {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
height: 30px;
|
|
||||||
padding: 0 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--stats-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.2;
|
|
||||||
text-decoration: none;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-hero-option"]:hover,
|
|
||||||
[data-page="stats"] [data-slot="model-hero-option"]:focus-visible {
|
|
||||||
background: var(--stats-layer-2);
|
|
||||||
color: var(--stats-text);
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-hero-option"][data-current="true"] {
|
|
||||||
background: var(--stats-layer-2);
|
|
||||||
color: var(--stats-text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-hero-separator"] {
|
[data-page="stats"] [data-slot="model-hero-separator"] {
|
||||||
@@ -5533,6 +5533,14 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] a[data-slot="compare-home-crumb"] {
|
||||||
|
color: var(--stats-hero-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-component="compare-home-breadcrumb"] [data-slot="compare-home-crumb"][data-current="true"] {
|
||||||
|
color: var(--stats-text);
|
||||||
|
}
|
||||||
|
|
||||||
[data-page="stats"] a[data-slot="compare-home-crumb"]:hover,
|
[data-page="stats"] a[data-slot="compare-home-crumb"]:hover,
|
||||||
[data-page="stats"] a[data-slot="compare-home-crumb"]:focus-visible {
|
[data-page="stats"] a[data-slot="compare-home-crumb"]:focus-visible {
|
||||||
color: var(--stats-text);
|
color: var(--stats-text);
|
||||||
@@ -7876,6 +7884,10 @@
|
|||||||
max-width: min(100%, 240px);
|
max-width: min(100%, 240px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-component="model-hero-breadcrumb"] [data-slot="stats-breadcrumb-select-trigger"] {
|
||||||
|
max-width: min(100%, 240px);
|
||||||
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="model-hero-title-row"] {
|
[data-page="stats"] [data-slot="model-hero-title-row"] {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
@@ -7967,10 +7979,6 @@
|
|||||||
min-width: 48px;
|
min-width: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="lab-hero-options"] {
|
|
||||||
max-width: calc(100vw - 48px);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-page="stats"] [data-component="lab-related-list"] {
|
[data-page="stats"] [data-component="lab-related-list"] {
|
||||||
grid-auto-columns: minmax(320px, 392px);
|
grid-auto-columns: minmax(320px, 392px);
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ export function isPromptEqual(a: Prompt, b: Prompt) {
|
|||||||
return a.every((part, i) => JSON.stringify(part) === JSON.stringify(b[i]))
|
return a.every((part, i) => JSON.stringify(part) === JSON.stringify(b[i]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isCommentItem(item: ContextItem) {
|
||||||
|
return !!item.comment?.trim()
|
||||||
|
}
|
||||||
|
|
||||||
export function createPromptState() {
|
export function createPromptState() {
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
prompt: clonePrompt(DEFAULT_PROMPT),
|
prompt: clonePrompt(DEFAULT_PROMPT),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
DEFAULT_THEMES,
|
DEFAULT_THEMES,
|
||||||
addTheme,
|
addTheme,
|
||||||
allThemes,
|
allThemes,
|
||||||
generateSubtleSyntax,
|
|
||||||
generateSyntax,
|
generateSyntax,
|
||||||
generateSystem,
|
generateSystem,
|
||||||
hasTheme,
|
hasTheme,
|
||||||
@@ -269,7 +268,6 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||||||
createEffect(() => renderer.setBackgroundColor(values().background))
|
createEffect(() => renderer.setBackgroundColor(values().background))
|
||||||
|
|
||||||
const syntax = createSyntaxStyleMemo(() => generateSyntax(values()))
|
const syntax = createSyntaxStyleMemo(() => generateSyntax(values()))
|
||||||
const subtleSyntax = createSyntaxStyleMemo(() => generateSubtleSyntax(values()))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: new Proxy(values(), {
|
theme: new Proxy(values(), {
|
||||||
@@ -284,7 +282,6 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||||||
all: allThemes,
|
all: allThemes,
|
||||||
has: hasTheme,
|
has: hasTheme,
|
||||||
syntax,
|
syntax,
|
||||||
subtleSyntax,
|
|
||||||
mode: () => store.mode,
|
mode: () => store.mode,
|
||||||
locked: () => store.lock !== undefined,
|
locked: () => store.lock !== undefined,
|
||||||
lock: () => pin(store.mode),
|
lock: () => pin(store.mode),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.17.18",
|
"version": "1.17.19",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -698,3 +698,104 @@ body[data-new-layout] #terminal-panel [data-component="tabs"][data-variant="norm
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[data-new-layout] #review-panel [data-component="tabs"][data-variant="normal"][data-orientation="horizontal"] {
|
||||||
|
[data-slot="tabs-list"] {
|
||||||
|
height: 52px;
|
||||||
|
padding-right: 12px;
|
||||||
|
gap: 8px;
|
||||||
|
--tabs-review-fade: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tabs-trigger-wrapper"] {
|
||||||
|
height: 28px;
|
||||||
|
padding-inline: 10px;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tabs-trigger"] {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has([data-slot="tabs-trigger-close-button"]) {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has([data-selected]) {
|
||||||
|
background-color: var(--v2-background-bg-layer-02);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tabs-trigger"] {
|
||||||
|
font-family: Inter, sans-serif;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 440;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: -0.04px;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
font-variation-settings: "slnt" 0;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
|
||||||
|
&[data-selected] {
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-fileicon-color,
|
||||||
|
.tab-fileicon-mono,
|
||||||
|
[data-slot="icon-svg"] {
|
||||||
|
margin-left: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview/temporary tabs: beat trigger + .text-14-medium upright defaults */
|
||||||
|
[data-slot="tabs-trigger"] .italic {
|
||||||
|
font-style: italic !important;
|
||||||
|
font-variation-settings: "slnt" -10 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-tabs-bar {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: 1px solid var(--border-weaker-base, var(--v2-border-border-weak));
|
||||||
|
background-color: var(--v2-background-bg-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-tabs-bar [data-slot="tabs-list"],
|
||||||
|
body[data-new-layout]
|
||||||
|
#review-panel
|
||||||
|
[data-component="tabs"][data-variant="normal"][data-orientation="horizontal"]
|
||||||
|
.session-review-v2-tabs-bar
|
||||||
|
[data-slot="tabs-list"] {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
width: auto;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-new-layout]
|
||||||
|
#review-panel
|
||||||
|
[data-component="tabs"]
|
||||||
|
.session-review-v2-tabs-bar
|
||||||
|
[data-slot="tabs-list"]
|
||||||
|
> .sticky {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-open-in-app-slot {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-open-in-app {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user