chore: merge dev into v2 (#36770)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: Victor Navarro <vn4varro@gmail.com> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Nabs <nabil@instafork.com> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com> Co-authored-by: Mark <geraint0923@users.noreply.github.com>
This commit is contained in:
parent
26e27c7a7f
commit
634386fe0f
116 changed files with 3764 additions and 1016 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
130
bun.lock
130
bun.lock
|
|
@ -32,7 +32,7 @@
|
||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@corvu/drawer": "catalog:",
|
"@corvu/drawer": "catalog:",
|
||||||
"@dnd-kit/abstract": "0.5.0",
|
"@dnd-kit/abstract": "0.5.0",
|
||||||
|
|
@ -157,7 +157,7 @@
|
||||||
},
|
},
|
||||||
"packages/codemode": {
|
"packages/codemode": {
|
||||||
"name": "@opencode-ai/codemode",
|
"name": "@opencode-ai/codemode",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "8.15.0",
|
"acorn": "8.15.0",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
|
@ -171,7 +171,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
|
|
@ -207,7 +207,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"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",
|
||||||
|
|
@ -234,7 +234,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"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",
|
||||||
|
|
@ -256,7 +256,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"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",
|
||||||
|
|
@ -280,7 +280,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/support": {
|
"packages/console/support": {
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
|
|
@ -300,7 +300,7 @@
|
||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
|
|
@ -308,7 +308,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",
|
||||||
|
|
@ -317,7 +317,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",
|
||||||
|
|
@ -354,7 +354,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",
|
||||||
|
|
@ -396,7 +396,7 @@
|
||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
|
@ -456,7 +456,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.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
|
@ -470,7 +470,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.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
|
|
@ -482,7 +482,7 @@
|
||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
|
@ -514,7 +514,7 @@
|
||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
|
|
@ -530,7 +530,7 @@
|
||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||||
},
|
},
|
||||||
|
|
@ -562,7 +562,7 @@
|
||||||
},
|
},
|
||||||
"packages/llm": {
|
"packages/llm": {
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@smithy/eventstream-codec": "4.2.14",
|
"@smithy/eventstream-codec": "4.2.14",
|
||||||
|
|
@ -581,7 +581,7 @@
|
||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
|
|
@ -592,7 +592,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",
|
||||||
|
|
@ -601,7 +601,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",
|
||||||
|
|
@ -656,7 +656,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",
|
||||||
|
|
@ -714,7 +714,7 @@
|
||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
|
|
@ -801,7 +801,7 @@
|
||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
|
|
@ -816,7 +816,7 @@
|
||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
|
@ -833,7 +833,7 @@
|
||||||
},
|
},
|
||||||
"packages/session-ui": {
|
"packages/session-ui": {
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
|
@ -894,7 +894,7 @@
|
||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
|
|
@ -907,9 +907,10 @@
|
||||||
},
|
},
|
||||||
"packages/stats/app": {
|
"packages/stats/app": {
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"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:",
|
||||||
|
|
@ -940,7 +941,7 @@
|
||||||
},
|
},
|
||||||
"packages/stats/core": {
|
"packages/stats/core": {
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"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",
|
||||||
|
|
@ -959,7 +960,7 @@
|
||||||
},
|
},
|
||||||
"packages/stats/server": {
|
"packages/stats/server": {
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
"@aws-sdk/client-firehose": "3.933.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
|
|
@ -1000,7 +1001,7 @@
|
||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
|
@ -1030,7 +1031,7 @@
|
||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
|
|
@ -1081,7 +1082,7 @@
|
||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
|
|
@ -1152,6 +1153,7 @@
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||||
|
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||||
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
|
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
|
||||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
||||||
|
|
@ -1159,10 +1161,8 @@
|
||||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||||
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
|
|
||||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||||
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
|
|
||||||
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|
@ -1198,7 +1198,7 @@
|
||||||
"@solidjs/router": "0.15.4",
|
"@solidjs/router": "0.15.4",
|
||||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"@tanstack/solid-virtual": "3.13.28",
|
"@tanstack/solid-virtual": "3.13.32",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@types/bun": "1.3.13",
|
"@types/bun": "1.3.13",
|
||||||
|
|
@ -1218,7 +1218,7 @@
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"marked": "17.0.1",
|
"marked": "17.0.6",
|
||||||
"marked-shiki": "1.2.1",
|
"marked-shiki": "1.2.1",
|
||||||
"opentui-spinner": "0.0.7",
|
"opentui-spinner": "0.0.7",
|
||||||
"remeda": "2.26.0",
|
"remeda": "2.26.0",
|
||||||
|
|
@ -1260,7 +1260,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=="],
|
||||||
|
|
||||||
|
|
@ -1270,7 +1270,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=="],
|
||||||
|
|
||||||
|
|
@ -3122,7 +3122,7 @@
|
||||||
|
|
||||||
"@tanstack/solid-query": ["@tanstack/solid-query@5.91.4", "", { "dependencies": { "@tanstack/query-core": "5.91.2" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-oCEgn8iT7WnF/7ISd7usBpUK1C9EdvQfg8ZUpKNKZ4edVClICZrCX6f3/Bp8ZlwQnL21KLc2rp+CejEuehlRxg=="],
|
"@tanstack/solid-query": ["@tanstack/solid-query@5.91.4", "", { "dependencies": { "@tanstack/query-core": "5.91.2" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-oCEgn8iT7WnF/7ISd7usBpUK1C9EdvQfg8ZUpKNKZ4edVClICZrCX6f3/Bp8ZlwQnL21KLc2rp+CejEuehlRxg=="],
|
||||||
|
|
||||||
"@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.28", "", { "dependencies": { "@tanstack/virtual-core": "3.17.0" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-kRuOEL5orH/rzGgxNgfgOttsgV6cgrUeupVtrHMITb5p0rZ3hnxhbu/lhKcR9+7x+EJdfUtJIb2CVC85mlw15g=="],
|
"@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.32", "", { "dependencies": { "@tanstack/virtual-core": "3.17.3" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-yhX4A4Kgn+wyTg6Mmu8+zwoMTwjz4K1ucvLfRJ8f0rPGDDAIqSaf0v6oU0yT9+SvrjmUaZQ0VX7g4byexbhNng=="],
|
||||||
|
|
||||||
"@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.13", "", { "dependencies": { "@tanstack/router-core": "1.171.14", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.16", "seroval": "^1.5.4" } }, "sha512-o37M3msIK5ec87kPrIYJWXb1XPnjIe5/jrkGLXiXpFuVL99z7mhoBCzftKtVPtzqI8EElnRE/VGFYT9BHNnWcw=="],
|
"@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.13", "", { "dependencies": { "@tanstack/router-core": "1.171.14", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.16", "seroval": "^1.5.4" } }, "sha512-o37M3msIK5ec87kPrIYJWXb1XPnjIe5/jrkGLXiXpFuVL99z7mhoBCzftKtVPtzqI8EElnRE/VGFYT9BHNnWcw=="],
|
||||||
|
|
||||||
|
|
@ -3136,7 +3136,7 @@
|
||||||
|
|
||||||
"@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
|
"@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
|
||||||
|
|
||||||
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.0", "", {}, "sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ=="],
|
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="],
|
||||||
|
|
||||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="],
|
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="],
|
||||||
|
|
||||||
|
|
@ -4344,7 +4344,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=="],
|
||||||
|
|
||||||
|
|
@ -4854,7 +4854,7 @@
|
||||||
|
|
||||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||||
|
|
||||||
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
"marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
|
||||||
|
|
||||||
"marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="],
|
"marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="],
|
||||||
|
|
||||||
|
|
@ -6340,7 +6340,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=="],
|
||||||
|
|
||||||
|
|
@ -6352,9 +6356,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=="],
|
||||||
|
|
||||||
|
|
@ -6844,7 +6848,7 @@
|
||||||
|
|
||||||
"@opencode-ai/console-mail/@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="],
|
"@opencode-ai/console-mail/@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
|
|
@ -6884,6 +6888,8 @@
|
||||||
|
|
||||||
"@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
"@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||||
|
|
||||||
|
"@opentui/core/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||||
|
|
||||||
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||||
|
|
||||||
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
||||||
|
|
@ -7230,6 +7236,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=="],
|
||||||
|
|
@ -7534,7 +7544,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=="],
|
||||||
|
|
||||||
|
|
@ -8226,6 +8236,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=="],
|
||||||
|
|
@ -8470,6 +8484,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=="],
|
||||||
|
|
@ -8782,6 +8804,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=="],
|
||||||
|
|
@ -9116,6 +9142,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=="],
|
||||||
|
|
||||||
"@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="],
|
"@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="],
|
||||||
|
|
@ -9302,6 +9330,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=="],
|
||||||
|
|
@ -9402,6 +9434,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=="],
|
||||||
|
|
|
||||||
|
|
@ -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="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
"@opentui/core": "0.4.3",
|
"@opentui/core": "0.4.3",
|
||||||
"@opentui/keymap": "0.4.3",
|
"@opentui/keymap": "0.4.3",
|
||||||
"@opentui/solid": "0.4.3",
|
"@opentui/solid": "0.4.3",
|
||||||
"@tanstack/solid-virtual": "3.13.28",
|
"@tanstack/solid-virtual": "3.13.32",
|
||||||
"@shikijs/stream": "4.2.0",
|
"@shikijs/stream": "4.2.0",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"marked": "17.0.1",
|
"marked": "17.0.6",
|
||||||
"marked-shiki": "1.2.1",
|
"marked-shiki": "1.2.1",
|
||||||
"remend": "1.3.0",
|
"remend": "1.3.0",
|
||||||
"@playwright/test": "1.59.1",
|
"@playwright/test": "1.59.1",
|
||||||
|
|
@ -161,10 +161,9 @@
|
||||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
||||||
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
||||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||||
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
|
|
||||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||||
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
|
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch"
|
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { expect, test, type Page } from "@playwright/test"
|
import { expect, test, type Page } from "@playwright/test"
|
||||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||||
|
import { installSseTransport } from "../utils/sse-transport"
|
||||||
import { expectSessionTitle } from "../utils/waits"
|
import { expectSessionTitle } from "../utils/waits"
|
||||||
|
|
||||||
const directory = "C:/OpenCode/RequestDocks"
|
const directory = "C:/OpenCode/RequestDocks"
|
||||||
|
|
@ -100,6 +101,67 @@ test("shows a pending permission dock", async ({ page }) => {
|
||||||
expect(request.postDataJSON()).toEqual({ response: "once" })
|
expect(request.postDataJSON()).toEqual({ response: "once" })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("restores the draft caret before typing after a request dock closes", async ({ page }) => {
|
||||||
|
const transport = await installSseTransport(page, {
|
||||||
|
server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`,
|
||||||
|
retry: 20,
|
||||||
|
})
|
||||||
|
await mockServer(page, { questions: [] })
|
||||||
|
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||||
|
await transport.waitForConnection()
|
||||||
|
await expectSessionTitle(page, title)
|
||||||
|
|
||||||
|
const editor = page.locator('[data-component="prompt-input"][contenteditable="true"]')
|
||||||
|
const draft = "keep the caret at the end"
|
||||||
|
await editor.fill(draft)
|
||||||
|
await page.evaluate(() => new Promise<void>((resolve) => requestAnimationFrame(() => resolve())))
|
||||||
|
for (let index = 0; index < 4; index++) await page.keyboard.press("ArrowLeft")
|
||||||
|
const cursor = draft.length - 4
|
||||||
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
editor.evaluate((element) => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection?.rangeCount || !element.contains(selection.anchorNode)) return -1
|
||||||
|
const range = selection.getRangeAt(0).cloneRange()
|
||||||
|
range.selectNodeContents(element)
|
||||||
|
range.setEnd(selection.anchorNode!, selection.anchorOffset)
|
||||||
|
return range.toString().length
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.toBe(cursor)
|
||||||
|
await transport.send({
|
||||||
|
directory,
|
||||||
|
payload: {
|
||||||
|
type: "question.asked",
|
||||||
|
properties: {
|
||||||
|
id: "question-caret",
|
||||||
|
sessionID,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
header: "Continue",
|
||||||
|
question: "Continue?",
|
||||||
|
options: [{ label: "Yes", description: "Continue the session" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tool: { messageID: "message-caret", callID: "call-caret" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const question = page.locator('[data-component="dock-prompt"][data-kind="question"]')
|
||||||
|
await expect(question).toBeVisible()
|
||||||
|
await expect(editor).toHaveCount(0)
|
||||||
|
|
||||||
|
await transport.send({
|
||||||
|
directory,
|
||||||
|
payload: { type: "question.rejected", properties: { sessionID, requestID: "question-caret" } },
|
||||||
|
})
|
||||||
|
await expect(question).toHaveCount(0)
|
||||||
|
await expect(editor).toBeVisible()
|
||||||
|
await page.keyboard.press("x")
|
||||||
|
|
||||||
|
await expect(editor).toHaveText(`${draft.slice(0, cursor)}x${draft.slice(cursor)}`)
|
||||||
|
})
|
||||||
|
|
||||||
async function mockServer(
|
async function mockServer(
|
||||||
page: Page,
|
page: Page,
|
||||||
requests: {
|
requests: {
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,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.20",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
|
||||||
156
packages/app/src/components/dialog-edit-project-v2.tsx
Normal file
156
packages/app/src/components/dialog-edit-project-v2.tsx
Normal file
|
|
@ -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>
|
||||||
|
|
|
||||||
119
packages/app/src/components/edit-project.ts
Normal file
119
packages/app/src/components/edit-project.ts
Normal file
|
|
@ -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,
|
||||||
|
|
@ -632,7 +633,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
|
const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
savedCursor = currentCursor()
|
const cursor = currentCursor()
|
||||||
|
savedCursor = cursor
|
||||||
|
if (cursor !== null && cursor !== prompt.cursor()) prompt.set(prompt.current(), cursor)
|
||||||
closePopover()
|
closePopover()
|
||||||
setComposing(false)
|
setComposing(false)
|
||||||
}
|
}
|
||||||
|
|
@ -1626,7 +1629,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 +1639,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 +1649,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 +1867,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 +1877,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 +2358,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,60 +1,167 @@
|
||||||
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 }}>
|
||||||
<For each={props.attachments}>
|
<div
|
||||||
{(attachment) => (
|
data-slot="prompt-attachments-scroll"
|
||||||
<Tooltip value={attachment.filename} placement="top" contentClass="break-all">
|
classList={{
|
||||||
<div class="relative group">
|
"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}>
|
||||||
|
{(attachment) => {
|
||||||
|
const image = attachment.mime.startsWith("image/")
|
||||||
|
const media = () => (
|
||||||
<Show
|
<Show
|
||||||
when={attachment.mime.startsWith("image/")}
|
when={image}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={fallbackClass}>
|
<Show
|
||||||
<Icon name="folder" class="size-6 text-text-weak" />
|
when={props.newLayoutDesigns}
|
||||||
</div>
|
fallback={
|
||||||
|
<div class={fallbackClass}>
|
||||||
|
<Icon name="folder" class="size-6 text-text-weak" />
|
||||||
|
</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>
|
||||||
</div>
|
)
|
||||||
</Tooltip>
|
const remove = () => (
|
||||||
)}
|
<button
|
||||||
</For>
|
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>
|
||||||
|
</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>
|
||||||
|
</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>
|
</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>
|
||||||
<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]">
|
<TooltipV2
|
||||||
<Icon name="branch" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
placement="top"
|
||||||
<span class="min-w-0 truncate">{branch()}</span>
|
value={branch()}
|
||||||
</div>
|
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]">
|
||||||
|
<Icon name="branch" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
|
<span class="min-w-0 truncate">{branch()}</span>
|
||||||
|
</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"
|
||||||
|
|
|
||||||
98
packages/app/src/components/session/open-in-app-v2.tsx
Normal file
98
packages/app/src/components/session/open-in-app-v2.tsx
Normal file
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
229
packages/app/src/components/session/open-in-app.tsx
Normal file
229
packages/app/src/components/session/open-in-app.tsx
Normal file
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
36
packages/app/src/components/status-popover-indicator.test.ts
Normal file
36
packages/app/src/components/status-popover-indicator.test.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator"
|
||||||
|
|
||||||
|
describe("serverStatusDotClass", () => {
|
||||||
|
test("uses the success token while the server and services are healthy", () => {
|
||||||
|
expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: false })).toBe("bg-icon-success-base")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses the warning token for non-blocking issues while the server is online", () => {
|
||||||
|
expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: true })).toBe("bg-icon-warning-base")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses the critical token only after the server connection drops", () => {
|
||||||
|
expect(serverStatusDotClass({ ready: true, serverHealth: false, issue: false })).toBe("bg-icon-critical-base")
|
||||||
|
expect(serverStatusDotClass({ ready: true, serverHealth: false, issue: true })).toBe("bg-icon-critical-base")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("stays neutral before status is ready", () => {
|
||||||
|
expect(serverStatusDotClass({ ready: false, serverHealth: true, issue: false })).toBe("bg-border-weak-base")
|
||||||
|
expect(serverStatusDotClass({ ready: false, serverHealth: undefined, issue: false })).toBe("bg-border-weak-base")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("hasNonBlockingServiceIssue", () => {
|
||||||
|
test("detects MCP failures that do not block chatting", () => {
|
||||||
|
expect(hasNonBlockingServiceIssue({ mcp: ["failed"], lsp: [] })).toBe(true)
|
||||||
|
expect(hasNonBlockingServiceIssue({ mcp: ["needs_auth"], lsp: [] })).toBe(true)
|
||||||
|
expect(hasNonBlockingServiceIssue({ mcp: ["needs_client_registration"], lsp: [] })).toBe(true)
|
||||||
|
expect(hasNonBlockingServiceIssue({ mcp: ["connected", "disabled"], lsp: [] })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("detects LSP failures that do not block chatting", () => {
|
||||||
|
expect(hasNonBlockingServiceIssue({ mcp: [], lsp: ["error"] })).toBe(true)
|
||||||
|
expect(hasNonBlockingServiceIssue({ mcp: [], lsp: ["connected"] })).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
19
packages/app/src/components/status-popover-indicator.ts
Normal file
19
packages/app/src/components/status-popover-indicator.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { LspStatus, McpStatus } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
|
export function hasNonBlockingServiceIssue(input: {
|
||||||
|
mcp: Array<McpStatus["status"]>
|
||||||
|
lsp: Array<LspStatus["status"]>
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
input.mcp.some((status) => status !== "connected" && status !== "disabled") ||
|
||||||
|
input.lsp.some((status) => status === "error")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serverStatusDotClass(input: { ready: boolean; serverHealth: boolean | undefined; issue: boolean }) {
|
||||||
|
if (input.serverHealth === false) return "bg-icon-critical-base"
|
||||||
|
if (!input.ready || input.serverHealth === undefined) return "bg-border-weak-base"
|
||||||
|
if (input.issue) return "bg-icon-warning-base"
|
||||||
|
if (input.serverHealth === true) return "bg-icon-success-base"
|
||||||
|
return "bg-border-weak-base"
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import { ServerConnection, useServer } from "@/context/server"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
|
import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator"
|
||||||
|
|
||||||
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
|
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
|
||||||
const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody })))
|
const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody })))
|
||||||
|
|
@ -19,16 +20,14 @@ export function StatusPopover() {
|
||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const [shown, setShown] = createSignal(false)
|
const [shown, setShown] = createSignal(false)
|
||||||
const ready = createMemo(() => global.servers.health[server.key]?.healthy === false || sync().data.mcp_ready)
|
const serverHealth = () => global.servers.health[server.key]?.healthy
|
||||||
const mcpIssue = createMemo(() => {
|
const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready))
|
||||||
const mcp = Object.values(sync().data.mcp ?? {})
|
const issue = createMemo(() =>
|
||||||
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
|
hasNonBlockingServiceIssue({
|
||||||
const warn = mcp.some((item) => item.status === "needs_auth")
|
mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status),
|
||||||
if (failed) return "critical" as const
|
lsp: (sync().data.lsp ?? []).map((item) => item.status),
|
||||||
if (warn) return "warning" as const
|
}),
|
||||||
})
|
)
|
||||||
const serverHealthy = () => global.servers.health[server.key]?.healthy === true
|
|
||||||
const healthy = createMemo(() => global.servers.health[server.key]?.healthy === true && !mcpIssue())
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
|
|
@ -47,13 +46,11 @@ export function StatusPopover() {
|
||||||
<Icon name={shown() ? "status-active" : "status"} size="small" />
|
<Icon name={shown() ? "status-active" : "status"} size="small" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
classList={{
|
class={`absolute -top-px -right-px size-1.5 rounded-full ${serverStatusDotClass({
|
||||||
"absolute -top-px -right-px size-1.5 rounded-full": true,
|
ready: ready(),
|
||||||
"bg-icon-success-base": ready() && healthy(),
|
serverHealth: serverHealth(),
|
||||||
"bg-icon-warning-base": ready() && serverHealthy() && mcpIssue() === "warning",
|
issue: issue(),
|
||||||
"bg-icon-critical-base": serverHealthy() || (ready() && serverHealthy() && mcpIssue() === "critical"),
|
})}`}
|
||||||
"bg-border-weak-base": serverHealthy() || !ready(),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -87,21 +84,18 @@ function DirectoryStatusPopover() {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const [shown, setShown] = createSignal(false)
|
const [shown, setShown] = createSignal(false)
|
||||||
const serverHealth = () => global.servers.health[ServerConnection.key(server().server)]?.healthy
|
const serverHealth = () => global.servers.health[ServerConnection.key(server().server)]?.healthy
|
||||||
const ready = createMemo(() => serverHealth() === false || sync().data.mcp_ready)
|
const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready))
|
||||||
const mcpIssue = createMemo(() => {
|
const issue = createMemo(() =>
|
||||||
const mcp = Object.values(sync().data.mcp ?? {})
|
hasNonBlockingServiceIssue({
|
||||||
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
|
mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status),
|
||||||
const warn = mcp.some((item) => item.status === "needs_auth")
|
lsp: (sync().data.lsp ?? []).map((item) => item.status),
|
||||||
if (failed) return "critical" as const
|
}),
|
||||||
if (warn) return "warning" as const
|
)
|
||||||
})
|
|
||||||
const healthy = createMemo(() => serverHealth() === true && !mcpIssue())
|
|
||||||
const state = createMemo<StatusPopoverState>(() => ({
|
const state = createMemo<StatusPopoverState>(() => ({
|
||||||
shown: shown(),
|
shown: shown(),
|
||||||
ready: ready(),
|
ready: ready(),
|
||||||
healthy: healthy(),
|
|
||||||
serverHealth: serverHealth(),
|
serverHealth: serverHealth(),
|
||||||
issue: mcpIssue(),
|
issue: issue(),
|
||||||
label: language.t("status.popover.trigger"),
|
label: language.t("status.popover.trigger"),
|
||||||
onOpenChange: setShown,
|
onOpenChange: setShown,
|
||||||
body: () => (
|
body: () => (
|
||||||
|
|
@ -123,8 +117,8 @@ function ServerStatusPopover() {
|
||||||
const state = createMemo<StatusPopoverState>(() => ({
|
const state = createMemo<StatusPopoverState>(() => ({
|
||||||
shown: shown(),
|
shown: shown(),
|
||||||
ready: serverHealth() !== undefined,
|
ready: serverHealth() !== undefined,
|
||||||
healthy: serverHealth() === true,
|
|
||||||
serverHealth: serverHealth(),
|
serverHealth: serverHealth(),
|
||||||
|
issue: false,
|
||||||
label: language.t("status.popover.trigger"),
|
label: language.t("status.popover.trigger"),
|
||||||
onOpenChange: setShown,
|
onOpenChange: setShown,
|
||||||
body: () => (
|
body: () => (
|
||||||
|
|
@ -140,9 +134,8 @@ function ServerStatusPopover() {
|
||||||
type StatusPopoverState = {
|
type StatusPopoverState = {
|
||||||
shown: boolean
|
shown: boolean
|
||||||
ready: boolean
|
ready: boolean
|
||||||
healthy: boolean
|
|
||||||
serverHealth: boolean | undefined
|
serverHealth: boolean | undefined
|
||||||
issue?: "critical" | "warning"
|
issue: boolean
|
||||||
label: string
|
label: string
|
||||||
onOpenChange: (value: boolean) => void
|
onOpenChange: (value: boolean) => void
|
||||||
body: () => JSX.Element
|
body: () => JSX.Element
|
||||||
|
|
@ -161,16 +154,6 @@ function StatusPopoverBody(props: { shown: boolean; children: JSX.Element }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusPopoverView(props: { state: StatusPopoverState }) {
|
function StatusPopoverView(props: { state: StatusPopoverState }) {
|
||||||
const statusDotClass = () => ({
|
|
||||||
"absolute rounded-full": true,
|
|
||||||
"bg-icon-success-base": props.state.ready && props.state.healthy,
|
|
||||||
"bg-icon-warning-base": props.state.ready && props.state.serverHealth === true && props.state.issue === "warning",
|
|
||||||
"bg-icon-critical-base":
|
|
||||||
props.state.serverHealth === false ||
|
|
||||||
(props.state.ready && props.state.serverHealth === true && props.state.issue === "critical"),
|
|
||||||
"bg-border-weak-base": props.state.serverHealth === undefined || !props.state.ready,
|
|
||||||
})
|
|
||||||
|
|
||||||
const popoverProps = {
|
const popoverProps = {
|
||||||
class:
|
class:
|
||||||
"[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl",
|
"[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl",
|
||||||
|
|
@ -195,8 +178,7 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
|
||||||
<div class="relative size-4">
|
<div class="relative size-4">
|
||||||
<IconV2 name={props.state.shown ? "status-active" : "status"} />
|
<IconV2 name={props.state.shown ? "status-active" : "status"} />
|
||||||
<div
|
<div
|
||||||
classList={statusDotClass()}
|
class={`absolute -top-1 -right-1 size-2 rounded-full border border-[var(--v2-background-bg-deep)] ${serverStatusDotClass(props.state)}`}
|
||||||
class="-top-1 -right-1 size-2 border border-[var(--v2-background-bg-deep)]"
|
|
||||||
/>
|
/>
|
||||||
</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
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,7 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
||||||
// background without navigating, matching browser conventions.
|
// 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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -57,6 +58,8 @@ import { useSync } from "@/context/sync"
|
||||||
import { useTabs } from "@/context/tabs"
|
import { useTabs } from "@/context/tabs"
|
||||||
import { TerminalProvider, useTerminal } from "@/context/terminal"
|
import { TerminalProvider, useTerminal } from "@/context/terminal"
|
||||||
import { PromptInput } from "@/components/prompt-input"
|
import { PromptInput } from "@/components/prompt-input"
|
||||||
|
import { setCursorPosition } from "@/components/prompt-input/editor-dom"
|
||||||
|
import { promptLength } from "@/components/prompt-input/history"
|
||||||
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
||||||
import {
|
import {
|
||||||
createPromptInputController,
|
createPromptInputController,
|
||||||
|
|
@ -79,6 +82,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"
|
||||||
|
|
@ -1015,7 +1019,10 @@ export default function Page() {
|
||||||
|
|
||||||
if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
|
if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
|
||||||
if (composer.blocked() || isChildSession()) return
|
if (composer.blocked() || isChildSession()) return
|
||||||
inputRef?.focus()
|
const input = inputRef
|
||||||
|
if (!input) return
|
||||||
|
input.focus()
|
||||||
|
setCursorPosition(input, prompt.cursor() ?? promptLength(prompt.current()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1867,7 +1874,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
|
||||||
|
|
@ -2232,6 +2262,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 { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
import type { FileDiffInfo, 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"
|
||||||
|
|
@ -47,6 +65,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
|
||||||
|
|
@ -60,7 +79,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
|
||||||
|
|
@ -92,11 +113,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)
|
||||||
|
|
@ -153,6 +172,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
|
||||||
|
|
@ -170,10 +190,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)
|
||||||
|
|
@ -196,10 +214,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)
|
||||||
|
|
@ -285,72 +319,283 @@ export function SessionSidePanel(props: {
|
||||||
"bg-background-base": !settings.general.newLayoutDesigns(),
|
"bg-background-base": !settings.general.newLayoutDesigns(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DragDropProvider
|
<Show
|
||||||
onDragStart={handleDragStart}
|
when={props.fileBrowserState}
|
||||||
onDragEnd={handleDragEnd}
|
fallback={
|
||||||
onDragOver={handleDragOver}
|
<DragDropProvider
|
||||||
collisionDetector={closestCenter}
|
onDragStart={handleDragStart}
|
||||||
>
|
onDragEnd={handleDragEnd}
|
||||||
<DragDropSensors />
|
onDragOver={handleDragOver}
|
||||||
<ConstrainDragYAxis />
|
collisionDetector={closestCenter}
|
||||||
<Tabs value={activeTab()} onChange={activateTab}>
|
>
|
||||||
<div class="sticky top-0 shrink-0 flex">
|
<DragDropSensors />
|
||||||
<Tabs.List
|
<ConstrainDragYAxis />
|
||||||
ref={(el: HTMLDivElement) => {
|
<Tabs value={activeTab()} onChange={activateTab}>
|
||||||
const stop = createFileTabListSync({ el, contextOpen })
|
<div class="sticky top-0 shrink-0 flex">
|
||||||
onCleanup(stop)
|
<Tabs.List
|
||||||
}}
|
ref={(el: HTMLDivElement) => {
|
||||||
>
|
const stop = createFileTabListSync({ el, contextOpen })
|
||||||
<Show when={reviewTab() && props.canReview()}>
|
onCleanup(stop)
|
||||||
<Tabs.Trigger
|
}}
|
||||||
value="review"
|
|
||||||
id={reviewTabID}
|
|
||||||
aria-controls={activeTab() === "review" ? reviewTabPanelID : undefined}
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1.5">
|
<Show when={reviewTab() && props.canReview()}>
|
||||||
<div>{language.t("session.tab.review")}</div>
|
<Tabs.Trigger
|
||||||
<Show when={props.hasReview()}>
|
value="review"
|
||||||
<div>{props.reviewCount()}</div>
|
id={reviewTabID}
|
||||||
</Show>
|
aria-controls={activeTab() === "review" ? reviewTabPanelID : undefined}
|
||||||
</div>
|
>
|
||||||
</Tabs.Trigger>
|
<div class="flex items-center gap-1.5">
|
||||||
</Show>
|
<div>{language.t("session.tab.review")}</div>
|
||||||
<Show when={contextOpen()}>
|
<Show when={props.hasReview()}>
|
||||||
<Tabs.Trigger
|
<div>{props.reviewCount()}</div>
|
||||||
value="context"
|
</Show>
|
||||||
closeButton={
|
</div>
|
||||||
|
</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>
|
||||||
|
<SortableProvider ids={openedTabs()}>
|
||||||
|
<For each={panelTabs()}>
|
||||||
|
{(tab) => (
|
||||||
|
<Show
|
||||||
|
when={tab === SESSION_OPEN_FILE_TAB}
|
||||||
|
fallback={
|
||||||
|
<SortableTab
|
||||||
|
tab={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>
|
||||||
|
</SortableProvider>
|
||||||
|
<div
|
||||||
|
class="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3"
|
||||||
|
classList={{
|
||||||
|
"bg-v2-background-bg-base": settings.general.newLayoutDesigns(),
|
||||||
|
"bg-background-stronger": !settings.general.newLayoutDesigns(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
title={language.t("common.closeTab")}
|
title={language.t("command.file.open")}
|
||||||
keybind={command.keybind("tab.close")}
|
keybind={command.keybind("file.open")}
|
||||||
placement="bottom"
|
class="flex items-center"
|
||||||
gutter={10}
|
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="close-small"
|
icon="plus-small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-5 w-5"
|
iconSize="large"
|
||||||
onClick={() => tabs().close("context")}
|
class="!rounded-md"
|
||||||
aria-label={language.t("common.closeTab")}
|
onClick={() => {
|
||||||
|
void import("@/components/dialog-select-file").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
aria-label={language.t("command.file.open")}
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
}
|
|
||||||
hideCloseButton
|
|
||||||
onMiddleClick={() => tabs().close("context")}
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<SessionContextUsage variant="indicator" />
|
|
||||||
<div>{language.t("session.tab.context")}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Trigger>
|
</Tabs.List>
|
||||||
|
</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>
|
||||||
<SortableProvider ids={openedTabs()}>
|
|
||||||
|
<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={activeFileTab()} keyed>
|
||||||
|
{(tab) => <FileTabContent tab={tab} />}
|
||||||
|
</Show>
|
||||||
|
</Tabs>
|
||||||
|
<DragOverlay>
|
||||||
|
<Show when={store.activeDraggable} keyed>
|
||||||
|
{(tab) => {
|
||||||
|
const path = file.pathFromTab(tab)
|
||||||
|
return (
|
||||||
|
<div data-component="tabs-drag-preview">
|
||||||
|
<Show when={path}>
|
||||||
|
{(p) => <FileVisual active path={p()} temporary={temporaryTab() === tab} />}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Show>
|
||||||
|
</DragOverlay>
|
||||||
|
</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()}>
|
<For each={panelTabs()}>
|
||||||
{(tab) => (
|
{(tab) => (
|
||||||
<Show
|
<Show
|
||||||
when={tab === SESSION_OPEN_FILE_TAB}
|
when={tab === SESSION_OPEN_FILE_TAB}
|
||||||
fallback={
|
fallback={
|
||||||
<SortableTab
|
<SortableTabV2
|
||||||
tab={tab}
|
tab={tab}
|
||||||
|
index={() => tabs().all().indexOf(tab)}
|
||||||
temporary={temporaryTab() === tab}
|
temporary={temporaryTab() === tab}
|
||||||
onTabClose={tabs().close}
|
onTabClose={tabs().close}
|
||||||
onTabDoubleClick={temporaryTab() === tab ? openTab : undefined}
|
onTabDoubleClick={temporaryTab() === tab ? openTab : undefined}
|
||||||
|
|
@ -386,106 +631,104 @@ export function SessionSidePanel(props: {
|
||||||
</Show>
|
</Show>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</SortableProvider>
|
<div
|
||||||
<div
|
class="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center"
|
||||||
class="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3"
|
classList={{
|
||||||
classList={{
|
"bg-v2-background-bg-base": settings.general.newLayoutDesigns(),
|
||||||
"bg-v2-background-bg-base": settings.general.newLayoutDesigns(),
|
"bg-background-stronger": !settings.general.newLayoutDesigns(),
|
||||||
"bg-background-stronger": !settings.general.newLayoutDesigns(),
|
}}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TooltipKeybind
|
|
||||||
title={language.t("command.file.open")}
|
|
||||||
keybind={command.keybind("file.open")}
|
|
||||||
class="flex items-center"
|
|
||||||
>
|
>
|
||||||
<IconButton
|
<TooltipV2
|
||||||
icon="plus-small"
|
value={
|
||||||
variant="ghost"
|
<>
|
||||||
iconSize="large"
|
{language.t("command.file.open")}
|
||||||
class="!rounded-md"
|
<Show when={openFileKeybind().length > 0}>
|
||||||
onClick={() => {
|
<KeybindV2 keys={openFileKeybind()} variant="neutral" />
|
||||||
if (props.fileBrowserState) {
|
</Show>
|
||||||
openFileBrowser()
|
</>
|
||||||
return
|
}
|
||||||
}
|
placement="bottom"
|
||||||
void import("@/components/dialog-select-file").then((x) => {
|
class="flex items-center"
|
||||||
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
|
>
|
||||||
})
|
<IconButtonV2
|
||||||
}}
|
icon={<Icon name="plus-small" />}
|
||||||
aria-label={language.t("command.file.open")}
|
variant="ghost-muted"
|
||||||
/>
|
size="large"
|
||||||
</TooltipKeybind>
|
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>
|
||||||
</Tabs.List>
|
|
||||||
</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>
|
</div>
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={activeTab() === "empty"}>
|
<Show when={reviewTab() && props.canReview() && activeTab() === "review"}>
|
||||||
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
|
<div
|
||||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
id={reviewTabPanelID}
|
||||||
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-6">
|
role="tabpanel"
|
||||||
<Mark class="w-14 opacity-10" />
|
aria-labelledby={reviewTabID}
|
||||||
<div class="text-14-regular text-text-weak max-w-56">
|
tabIndex={props.reviewHasFocusableContent() ? undefined : 0}
|
||||||
{language.t("session.files.selectToOpen")}
|
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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Tabs.Content>
|
||||||
</Tabs.Content>
|
</Show>
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={activeTab() === "context"}>
|
<Show when={activeTab() === "context"}>
|
||||||
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
<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">
|
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||||
<SessionContextTab />
|
<SessionContextTab />
|
||||||
</div>
|
|
||||||
</Tabs.Content>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={browserTab()}>
|
|
||||||
<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} />}
|
|
||||||
</Show>
|
|
||||||
</Tabs>
|
|
||||||
<DragOverlay>
|
|
||||||
<Show when={store.activeDraggable} keyed>
|
|
||||||
{(tab) => {
|
|
||||||
const path = file.pathFromTab(tab)
|
|
||||||
return (
|
|
||||||
<div data-component="tabs-drag-preview">
|
|
||||||
<Show when={path}>
|
|
||||||
{(p) => <FileVisual active path={p()} temporary={temporaryTab() === tab} />}
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
</Tabs.Content>
|
||||||
}}
|
</Show>
|
||||||
</Show>
|
|
||||||
</DragOverlay>
|
<Show when={fileBrowserMounted()}>
|
||||||
</DragDropProvider>
|
<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>
|
||||||
|
|
@ -513,10 +756,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" }}>
|
||||||
{props.reviewCount()}{" "}
|
<Show
|
||||||
{language.t(
|
when={settings.general.newLayoutDesigns()}
|
||||||
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
fallback={
|
||||||
)}
|
<>
|
||||||
|
{props.reviewCount()}{" "}
|
||||||
|
{language.t(
|
||||||
|
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"
|
||||||
|
|
@ -65,6 +64,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.
|
||||||
|
|
@ -112,9 +113,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.
|
||||||
|
|
@ -126,7 +124,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,102 +88,92 @@ 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={false}
|
||||||
toolbar
|
sidebar={
|
||||||
toolbarStart={
|
<SessionReviewV2Sidebar
|
||||||
<>
|
open={sidebarOpened()}
|
||||||
<SessionReviewV2SidebarToggle opened={sidebarOpened()} onToggle={props.state.toggleSidebar} />
|
title={<span class="truncate">{title()}</span>}
|
||||||
<Show when={!sidebarOpened()}>
|
filter={filter()}
|
||||||
<SessionFilePanelV2Title>{title()}</SessionFilePanelV2Title>
|
onFilterChange={setFilter}
|
||||||
</Show>
|
onFilterKeyDown={onFilterKeyDown}
|
||||||
</>
|
filterAutofocus={props.placeholder}
|
||||||
}
|
filterRef={props.filterRef}
|
||||||
sidebar={
|
filterControls={resultsID}
|
||||||
<SessionReviewV2Sidebar
|
filterActiveDescendant={highlighted() ? optionID(highlighted()!) : undefined}
|
||||||
open={sidebarOpened()}
|
filterExpanded={query().length > 0 && files().length > 0}
|
||||||
title={<span class="truncate">{title()}</span>}
|
width={props.state.sidebarWidth()}
|
||||||
filter={filter()}
|
onWidthChange={props.state.resizeSidebar}
|
||||||
onFilterChange={setFilter}
|
>
|
||||||
onFilterKeyDown={onFilterKeyDown}
|
<Show
|
||||||
filterAutofocus={props.placeholder}
|
when={query()}
|
||||||
filterRef={props.filterRef}
|
fallback={
|
||||||
filterControls={resultsID}
|
<FileTreeV2
|
||||||
filterActiveDescendant={highlighted() ? optionID(highlighted()!) : undefined}
|
active={props.active}
|
||||||
filterExpanded={query().length > 0 && files().length > 0}
|
kinds={props.kinds}
|
||||||
width={props.state.sidebarWidth()}
|
onFileClick={(node) => props.onSelect(node.path)}
|
||||||
onWidthChange={props.state.resizeSidebar}
|
onFileDoubleClick={(node) => props.onSelectPermanent(node.path)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={query()}
|
when={!loading()}
|
||||||
fallback={
|
fallback={
|
||||||
<FileTree
|
<div role="status" class="px-2 py-2 text-12-regular text-text-weak">
|
||||||
path=""
|
{language.t("common.loading")}
|
||||||
class="pt-1"
|
{language.t("common.loading.ellipsis")}
|
||||||
active={props.active}
|
</div>
|
||||||
kinds={props.kinds}
|
|
||||||
onFileClick={(node) => props.onSelect(node.path)}
|
|
||||||
onFileDoubleClick={(node) => props.onSelectPermanent(node.path)}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={!loading()}
|
when={files().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div role="status" class="px-2 py-2 text-12-regular text-text-weak">
|
<div role="status" class="px-2 py-2 text-12-regular text-text-weak">
|
||||||
{language.t("common.loading")}
|
{language.t("palette.empty")}
|
||||||
{language.t("common.loading.ellipsis")}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show
|
<SessionFileListV2
|
||||||
when={files().length > 0}
|
id={resultsID}
|
||||||
fallback={
|
role="listbox"
|
||||||
<div role="status" class="px-2 py-2 text-12-regular text-text-weak">
|
optionID={optionID}
|
||||||
{language.t("palette.empty")}
|
files={files()}
|
||||||
</div>
|
kinds={props.kinds}
|
||||||
}
|
active={props.active}
|
||||||
>
|
highlighted={highlighted()}
|
||||||
<SessionFileListV2
|
onFileClick={(path) => {
|
||||||
id={resultsID}
|
setExplicitHighlight(path)
|
||||||
role="listbox"
|
props.onSelect(path)
|
||||||
optionID={optionID}
|
}}
|
||||||
files={files()}
|
onFileDoubleClick={props.onSelectPermanent}
|
||||||
kinds={props.kinds}
|
/>
|
||||||
active={props.active}
|
|
||||||
highlighted={highlighted()}
|
|
||||||
onFileClick={(path) => {
|
|
||||||
setExplicitHighlight(path)
|
|
||||||
props.onSelect(path)
|
|
||||||
}}
|
|
||||||
onFileDoubleClick={props.onSelectPermanent}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</SessionReviewV2Sidebar>
|
</Show>
|
||||||
|
</SessionReviewV2Sidebar>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={!props.placeholder}
|
||||||
|
fallback={
|
||||||
|
<SessionFilePanelV2Empty>
|
||||||
|
<div class="flex flex-col items-center gap-3 text-center text-text-weak">
|
||||||
|
<Icon name="file-tree" size="large" />
|
||||||
|
<div class="text-14-medium text-text-strong">{language.t("command.file.open")}</div>
|
||||||
|
<div class="text-13-regular">{language.t("session.files.selectToOpen")}</div>
|
||||||
|
</div>
|
||||||
|
</SessionFilePanelV2Empty>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show
|
<div class="min-h-0 flex-1">
|
||||||
when={!props.placeholder}
|
<Show when={props.tab} keyed>
|
||||||
fallback={
|
{(tab) => <SessionFileView tab={tab} />}
|
||||||
<SessionFilePanelV2Empty>
|
</Show>
|
||||||
<div class="flex flex-col items-center gap-3 text-center text-text-weak">
|
</div>
|
||||||
<Icon name="file-tree" size="large" />
|
</Show>
|
||||||
<div class="text-14-medium text-text-strong">{language.t("command.file.open")}</div>
|
</SessionFilePanelV2>
|
||||||
<div class="text-13-regular">{language.t("session.files.selectToOpen")}</div>
|
|
||||||
</div>
|
|
||||||
</SessionFilePanelV2Empty>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="min-h-0 flex-1">
|
|
||||||
<Show when={props.tab} keyed>
|
|
||||||
{(tab) => <SessionFileView tab={tab} />}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</SessionFilePanelV2>
|
|
||||||
</Tabs.Content>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,32 @@
|
||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
|
import { createVirtualizer, defaultRangeExtractor, Virtualizer } from "@tanstack/solid-virtual"
|
||||||
import { createRoot, createSignal } from "solid-js"
|
import { createRoot, createSignal } from "solid-js"
|
||||||
import { filterVirtualIndexes } from "@/pages/session/timeline/virtual-items"
|
import { filterVirtualIndexes } from "@/pages/session/timeline/virtual-items"
|
||||||
|
|
||||||
|
test("end anchoring survives consecutive resizes when the first scroll write is clamped", () => {
|
||||||
|
const writes: { offset: number; adjustments?: number }[] = []
|
||||||
|
const virtualizer = new Virtualizer<HTMLDivElement, HTMLDivElement>({
|
||||||
|
count: 5,
|
||||||
|
estimateSize: () => 50,
|
||||||
|
initialOffset: 50,
|
||||||
|
initialRect: { width: 400, height: 200 },
|
||||||
|
anchorTo: "end",
|
||||||
|
scrollEndThreshold: 1,
|
||||||
|
getScrollElement: () => null,
|
||||||
|
scrollToFn: (offset, options) => writes.push({ offset, adjustments: options.adjustments }),
|
||||||
|
observeElementRect: () => {},
|
||||||
|
observeElementOffset: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
virtualizer.getTotalSize()
|
||||||
|
virtualizer.resizeItem(4, 120)
|
||||||
|
expect(writes).toEqual([{ offset: 50, adjustments: 70 }])
|
||||||
|
writes.length = 0
|
||||||
|
|
||||||
|
virtualizer.resizeItem(4, 200)
|
||||||
|
expect(writes).toEqual([{ offset: 120, adjustments: 80 }])
|
||||||
|
})
|
||||||
|
|
||||||
test("reactive count updates preserve measured row sizes", () => {
|
test("reactive count updates preserve measured row sizes", () => {
|
||||||
createRoot((dispose) => {
|
createRoot((dispose) => {
|
||||||
const [count, setCount] = createSignal(2)
|
const [count, setCount] = createSignal(2)
|
||||||
|
|
@ -42,23 +66,26 @@ test("initial rect projects rows before a scroll element connects", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("logical scroll offset includes pending measurement adjustments", () => {
|
test("clamps oversized offsets with scroll margin and padding changes", () => {
|
||||||
createRoot((dispose) => {
|
const options = (paddingEnd: number) => ({
|
||||||
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
|
count: 20,
|
||||||
count: 2,
|
estimateSize: () => 60,
|
||||||
getScrollElement: () => null,
|
initialOffset: Number.MAX_SAFE_INTEGER,
|
||||||
estimateSize: () => 60,
|
initialRect: { width: 800, height: 600 },
|
||||||
initialOffset: 100,
|
scrollMargin: 64,
|
||||||
initialRect: { width: 800, height: 60 },
|
paddingEnd,
|
||||||
})
|
overscan: 1,
|
||||||
|
getScrollElement: () => null,
|
||||||
virtualizer.getTotalSize()
|
scrollToFn: () => {},
|
||||||
virtualizer.resizeItem(0, 100)
|
observeElementRect: () => {},
|
||||||
|
observeElementOffset: () => {},
|
||||||
expect(virtualizer.scrollOffset).toBe(100)
|
|
||||||
expect(virtualizer.getLogicalScrollOffset()).toBe(140)
|
|
||||||
dispose()
|
|
||||||
})
|
})
|
||||||
|
const virtualizer = new Virtualizer<HTMLDivElement, HTMLDivElement>(options(64))
|
||||||
|
|
||||||
|
expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
|
||||||
|
|
||||||
|
virtualizer.setOptions(options(600))
|
||||||
|
expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([18, 19])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("stale pinned indexes do not produce missing virtual items after count shrinks", () => {
|
test("stale pinned indexes do not produce missing virtual items after count shrinks", () => {
|
||||||
|
|
|
||||||
|
|
@ -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.20",
|
||||||
"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.20",
|
||||||
"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.20",
|
||||||
"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.20",
|
||||||
"$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.20",
|
||||||
"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.20",
|
||||||
"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.20",
|
||||||
"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",
|
||||||
|
|
@ -110,7 +110,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",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"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.20",
|
||||||
"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.20",
|
||||||
"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.20",
|
||||||
"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.20",
|
||||||
"$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.20",
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"description": "Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",
|
"description": "Record and replay Effect HTTP and WebSocket 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.20",
|
||||||
"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.20",
|
||||||
"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",
|
||||||
|
|
@ -122,7 +122,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,
|
||||||
|
|
|
||||||
|
|
@ -283,8 +283,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
|
||||||
})
|
})
|
||||||
|
|
@ -416,6 +418,7 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
||||||
|
|
||||||
const requestInit = {
|
const requestInit = {
|
||||||
...init,
|
...init,
|
||||||
|
body: init?.body,
|
||||||
headers,
|
headers,
|
||||||
}
|
}
|
||||||
if (websocketFetch && parsed.pathname.endsWith("/responses")) return websocketFetch(url, requestInit)
|
if (websocketFetch && parsed.pathname.endsWith("/responses")) return websocketFetch(url, requestInit)
|
||||||
|
|
|
||||||
|
|
@ -1266,6 +1266,14 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||||
// @ts-expect-error dead V1 expects raw model IDs inside normalized ModelsDev data.
|
// @ts-expect-error dead V1 expects raw model IDs inside normalized ModelsDev data.
|
||||||
const id = `${model.id}-${mode}`
|
const id = `${model.id}-${mode}`
|
||||||
const base = fromModelsDevModel(provider, model)
|
const base = fromModelsDevModel(provider, model)
|
||||||
|
const providerOptions = (
|
||||||
|
opts as {
|
||||||
|
readonly provider?: {
|
||||||
|
readonly body?: Record<string, unknown>
|
||||||
|
readonly headers?: Record<string, string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).provider
|
||||||
models[id] = {
|
models[id] = {
|
||||||
...base,
|
...base,
|
||||||
id: ModelV2.ID.make(id),
|
id: ModelV2.ID.make(id),
|
||||||
|
|
@ -1273,18 +1281,8 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||||
name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
|
name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
|
||||||
// @ts-expect-error dead V1 expects raw mode costs inside normalized ModelsDev data.
|
// @ts-expect-error dead V1 expects raw mode costs inside normalized ModelsDev data.
|
||||||
cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
|
cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
|
||||||
// @ts-expect-error dead V1 expects raw mode provider bodies inside normalized ModelsDev data.
|
options: modeOptions(base, providerOptions?.body),
|
||||||
options: opts.provider?.body
|
headers: providerOptions?.headers ?? base.headers,
|
||||||
? Object.fromEntries(
|
|
||||||
// @ts-expect-error dead V1 expects raw mode provider bodies inside normalized ModelsDev data.
|
|
||||||
Object.entries(opts.provider.body).map(([k, v]) => [
|
|
||||||
k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
|
|
||||||
v,
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
: base.options,
|
|
||||||
// @ts-expect-error dead V1 expects raw mode headers inside normalized ModelsDev data.
|
|
||||||
headers: opts.provider?.headers ?? base.headers,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1298,6 +1296,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) => {
|
||||||
|
|
|
||||||
|
|
@ -761,7 +761,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) => [
|
||||||
|
|
@ -794,8 +794,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: {
|
||||||
|
|
@ -806,7 +806,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()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -1104,7 +1104,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
|
||||||
}
|
}
|
||||||
|
|
@ -1259,7 +1260,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)
|
||||||
|
|
|
||||||
|
|
@ -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,16 +149,32 @@ describe("plugin.codex", () => {
|
||||||
await enabled.dispose?.()
|
await enabled.dispose?.()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses Codex context limits for OAuth GPT models", async () => {
|
test("filters unsupported modes and uses Codex context limits for OAuth GPT models", async () => {
|
||||||
const hooks = await CodexAuthPlugin({} as never)
|
const hooks = await CodexAuthPlugin({} as never)
|
||||||
const limit = { context: 1_050_000, input: 922_000, output: 128_000 }
|
const limit = { context: 1_050_000, input: 922_000, output: 128_000 }
|
||||||
const provider = {
|
const provider = {
|
||||||
models: Object.fromEntries(
|
models: {
|
||||||
["gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].map((id) => [
|
...Object.fromEntries(
|
||||||
id,
|
["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, api: { id }, limit, cost: {} },
|
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)
|
const models = await hooks.provider!.models!(provider as never, { auth: { type: "oauth" } } as never)
|
||||||
|
|
@ -168,6 +184,9 @@ describe("plugin.codex", () => {
|
||||||
expect(models["gpt-5.6-sol"]?.limit).toEqual({ context: 500_000, input: 372_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-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.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(
|
expect(await hooks.provider!.models!(provider as never, { auth: { type: "api" } } as never)).toBe(
|
||||||
provider.models as never,
|
provider.models as never,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1339,16 +1339,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,
|
||||||
|
|
@ -1384,18 +1385,29 @@ test("mode cost preserves over-200k pricing from base model", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pro: {
|
||||||
|
provider: {
|
||||||
|
body: {
|
||||||
|
reasoning: { mode: "pro" },
|
||||||
|
service_tier: "priority",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as ModelsDevProvider
|
} as unknown as ModelsDevProvider
|
||||||
|
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,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,
|
||||||
|
|
@ -3372,6 +3409,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",
|
||||||
|
|
|
||||||
|
|
@ -1095,7 +1095,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
|
||||||
|
|
@ -1126,6 +1126,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
|
||||||
|
|
|
||||||
|
|
@ -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.20",
|
||||||
"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.20",
|
||||||
"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.20",
|
||||||
"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.20",
|
||||||
"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"] {
|
||||||
|
|
@ -1301,6 +1313,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"] {
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,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"
|
||||||
|
|
@ -58,7 +60,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"
|
||||||
|
|
||||||
|
|
@ -165,6 +167,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
|
||||||
|
|
@ -172,6 +175,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 {
|
||||||
|
|
@ -937,6 +950,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>
|
||||||
|
|
@ -1162,6 +1176,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()
|
||||||
|
|
@ -1183,6 +1198,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[]) ?? [])
|
||||||
|
|
@ -1239,35 +1256,59 @@ 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 (
|
||||||
<div
|
<Show
|
||||||
data-slot="user-message-attachment"
|
when={newLayout() && type === "file"}
|
||||||
data-type={type}
|
fallback={
|
||||||
data-clickable={type === "image" ? "true" : undefined}
|
<div
|
||||||
title={type === "file" ? name : undefined}
|
data-slot="user-message-attachment"
|
||||||
onClick={() => {
|
data-type={type}
|
||||||
if (type === "image") openImagePreview(file.url, name)
|
data-clickable={type === "image" ? "true" : undefined}
|
||||||
}}
|
title={type === "file" ? name : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
if (type === "image") openImagePreview(file.url, name)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={type === "image"}
|
||||||
|
fallback={
|
||||||
|
<div data-slot="user-message-attachment-file">
|
||||||
|
<FileIcon node={{ path: name, type: "file" }} />
|
||||||
|
<span data-slot="user-message-attachment-name">{name}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img data-slot="user-message-attachment-image" src={file.url} alt={name} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Show
|
<AttachmentCardV2
|
||||||
when={type === "image"}
|
title={getFilename(name)}
|
||||||
fallback={
|
hover={name}
|
||||||
<div data-slot="user-message-attachment-file">
|
clickable={!!props.actions?.openAttachment}
|
||||||
<FileIcon node={{ path: name, type: "file" }} />
|
onClick={() => props.actions?.openAttachment?.(file)}
|
||||||
<span data-slot="user-message-attachment-name">{name}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<img data-slot="user-message-attachment-image" src={file.url} alt={name} />
|
{typeLabel(name, file.mime)}
|
||||||
</Show>
|
</AttachmentCardV2>
|
||||||
</div>
|
</Show>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
||||||
58
packages/session-ui/src/v2/components/attachment-card-v2.css
Normal file
58
packages/session-ui/src/v2/components/attachment-card-v2.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/session-ui/src/v2/components/attachment-card-v2.tsx
Normal file
26
packages/session-ui/src/v2/components/attachment-card-v2.tsx
Normal file
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
27
packages/session-ui/src/v2/components/comment-card-v2.tsx
Normal file
27
packages/session-ui/src/v2/components/comment-card-v2.tsx
Normal file
|
|
@ -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.20",
|
||||||
"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.20",
|
||||||
"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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
71
packages/stats/app/src/routes/breadcrumb-select.tsx
Normal file
71
packages/stats/app/src/routes/breadcrumb-select.tsx
Normal file
|
|
@ -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.20",
|
||||||
"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.20",
|
||||||
"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.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.17.18",
|
"version": "1.17.20",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue