mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-30 14:46:05 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c38d260eb | ||
|
|
a4f25a94b4 | ||
|
|
ff0382e971 | ||
|
|
ac5380614f | ||
|
|
c08350b83e | ||
|
|
c5cf416633 | ||
|
|
3dbce3dade | ||
|
|
a277557a5f | ||
|
|
005bf088c9 | ||
|
|
eca5e68a5e | ||
|
|
5b11635d97 | ||
|
|
a9eda2e99e | ||
|
|
73009f0b09 | ||
|
|
f720490219 | ||
|
|
341c64cc97 | ||
|
|
1e17856ba4 | ||
|
|
a6f7fe739c | ||
|
|
02f50a6e80 | ||
|
|
7565e03536 | ||
|
|
035a5a1a67 |
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"bin": {
|
||||
"lildax": "./bin/lildax.cjs",
|
||||
},
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -221,7 +221,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -267,7 +267,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -287,7 +287,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -339,7 +339,7 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "6.12.0",
|
||||
"gitlab-ai-provider": "6.12.1",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
@@ -381,7 +381,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"effect": "catalog:",
|
||||
@@ -435,7 +435,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -449,7 +449,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -461,7 +461,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -493,7 +493,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -509,7 +509,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "4.0.0-beta.83",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||
@@ -540,7 +540,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
@@ -559,7 +559,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -632,7 +632,7 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "6.12.0",
|
||||
"gitlab-ai-provider": "6.12.1",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
@@ -690,7 +690,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -766,7 +766,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -781,7 +781,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
@@ -796,7 +796,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
|
||||
@@ -841,7 +841,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -854,7 +854,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -888,7 +888,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -907,7 +907,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -949,7 +949,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
@@ -976,7 +976,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -1002,6 +1002,7 @@
|
||||
"remend": "catalog:",
|
||||
"shiki": "catalog:",
|
||||
"solid-list": "catalog:",
|
||||
"solid-sonner": "catalog:",
|
||||
"strip-ansi": "7.1.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1027,7 +1028,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1146,6 +1147,7 @@
|
||||
"shiki": "4.2.0",
|
||||
"solid-js": "1.9.10",
|
||||
"solid-list": "0.3.0",
|
||||
"solid-sonner": "0.3.1",
|
||||
"sst": "4.13.1",
|
||||
"tailwindcss": "4.1.11",
|
||||
"typescript": "5.8.2",
|
||||
@@ -3855,7 +3857,7 @@
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.12.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-uGan7MQaNfmV5SH9wiMJkZ7D5QxM4KVPRcjIHtPCM5icZjgXl5mrHMjr3/e7TAHa9OjEJKT0a2FxzPgwRtN3PQ=="],
|
||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.12.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-Qn5iHqvjG8yktI5MWaUgdRR94l7O4WtYW0CAbhsCh1Tj0Fei/DeprOYPVyf4Nht1Ix6U2PXSYM32QOHI6Z2TDw=="],
|
||||
|
||||
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||
|
||||
@@ -5127,6 +5129,8 @@
|
||||
|
||||
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
|
||||
|
||||
"solid-sonner": ["solid-sonner@0.3.1", "", { "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-F/+zi9yKJTHh5hX1UGJfkDvyC+F34Vi3jgy44NJwOKCgic1QtAon0b1iT9OsDO77RTgR+PCil+3Y5B8T2Owy1Q=="],
|
||||
|
||||
"solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="],
|
||||
|
||||
"solid-transition-size": ["solid-transition-size@0.1.4", "", { "dependencies": { "@corvu/utils": "~0.3.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ocHVnbfy23CgfaH4cEUR/AFg0Y3CEL8Oh3n9Qv8OHFJgPh+zkmERKZQfi/xH5XvxDCizg8VjPrVUhiHB1Gza8g=="],
|
||||
|
||||
+6
-2
@@ -288,8 +288,12 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
server: {
|
||||
placement: { region: "aws:us-east-2" },
|
||||
transform: {
|
||||
worker: {
|
||||
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
|
||||
worker: (args) => {
|
||||
args.compatibilityFlags = $resolve(args.compatibilityFlags).apply((flags) => [
|
||||
...(flags ?? []),
|
||||
"global_fetch_strictly_public",
|
||||
])
|
||||
args.tailConsumers = [{ service: logProcessor.nodes.worker.scriptName }]
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-/ZtluLolVq1ZFAYgbDScze9GZR0NF9baRQXvfEoOnkI=",
|
||||
"aarch64-linux": "sha256-kdh7Dpira7cx9lvtozFsjLrDqMBrUMcgxkhfsqrutws=",
|
||||
"aarch64-darwin": "sha256-chPeMCX+8hvOQ32hTANIn7FmONsSv9t3dKTWTaHerjs=",
|
||||
"x86_64-darwin": "sha256-rMc0dDZB+YbXrjYwXTIt7QvHUxSp//wDJQ5hUdR+LYM="
|
||||
"x86_64-linux": "sha256-fK6zAHJC3ut/KUdfqLPxVMH7Z1yv65YZ7qcHF45kPas=",
|
||||
"aarch64-linux": "sha256-CSWZY/vVUCKzlwfisQQHqHjawUwL0BRc8Y9o2y/QZYA=",
|
||||
"aarch64-darwin": "sha256-IyFm5NbnU63BaOO/F4/v1exz3VbvkY96yjg9iun+O9Q=",
|
||||
"x86_64-darwin": "sha256-XnXFoUxqXmEx7AKx5YhaJuLY2NYMtI0ICafaenU5L6k="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
"@sentry/solid": "10.36.0",
|
||||
"@sentry/vite-plugin": "4.6.0",
|
||||
"solid-js": "1.9.10",
|
||||
"solid-sonner": "0.3.1",
|
||||
"vite-plugin-solid": "2.11.10",
|
||||
"@lydell/node-pty": "1.2.0-beta.12"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -19,9 +19,9 @@
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"test": "bun run test:unit && bun run test:browser",
|
||||
"test:unit": "bun test --only-failures --preload ./happydom.ts ./src",
|
||||
"test:unit": "bun test --conditions=solid --only-failures --preload ./happydom.ts ./src",
|
||||
"test:browser": "bun test --conditions=browser --preload ./happydom.ts ./test-browser",
|
||||
"test:unit:watch": "bun test --watch --preload ./happydom.ts ./src",
|
||||
"test:unit:watch": "bun test --conditions=solid --watch --preload ./happydom.ts ./src",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:local": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
|
||||
@@ -368,6 +368,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps):
|
||||
title: language.t("prompt.toast.pasteUnsupported.title"),
|
||||
description: language.t("prompt.toast.pasteUnsupported.description"),
|
||||
}),
|
||||
duplicate: () => showToast({ title: language.t("prompt.toast.attachmentDuplicate.title") }),
|
||||
onError: (error) =>
|
||||
showToast({
|
||||
variant: "error",
|
||||
|
||||
@@ -9,53 +9,86 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-titlebar-tab] {
|
||||
--tab-base: var(--v2-background-bg-deep);
|
||||
--tab-overlay: transparent;
|
||||
background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base);
|
||||
}
|
||||
|
||||
[data-titlebar-tab]:is(:hover, :has(> [data-slot="tab-link"]:focus-visible)):not([data-state="pressed"]):not(
|
||||
[data-dragging="true"]
|
||||
):not([data-editing="true"]) {
|
||||
--tab-base: var(--v2-background-bg-layer-02);
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-titlebar-tab]:is([data-state="pressed"], [data-dragging="true"], [data-editing="true"]) {
|
||||
--tab-base: var(--v2-background-bg-layer-02);
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-titlebar-tab]:is(:hover, [data-state="pressed"]) [data-slot="tab-close"] {
|
||||
background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base);
|
||||
}
|
||||
|
||||
[data-titlebar-tab][data-editing="true"] [data-slot="tab-close"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-titlebar-tab-list] {
|
||||
gap: 13.5px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
[data-titlebar-tab-slot] {
|
||||
--tab-separator: var(--v2-background-bg-layer-03);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-color-scheme="dark"] [data-titlebar-tab-slot] {
|
||||
--tab-separator: var(--v2-background-bg-layer-02);
|
||||
}
|
||||
|
||||
[data-titlebar-tab-slot]:not(:first-child):not([data-active="true"])::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: -6.75px;
|
||||
left: -3.75px;
|
||||
width: 1.5px;
|
||||
height: 12px;
|
||||
border-radius: 9999px;
|
||||
background: var(--v2-background-bg-layer-02);
|
||||
background: var(--tab-separator);
|
||||
}
|
||||
|
||||
[data-titlebar-tab-slot][data-active="true"] + [data-titlebar-tab-slot]::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-titlebar-tab] [data-slot="tab-close"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 28px;
|
||||
width: 16px;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(to right, transparent, var(--tab-bg));
|
||||
[data-titlebar-tab-slot]:not([data-active="true"]):has([data-titlebar-tab]:hover)::before,
|
||||
[data-titlebar-tab-slot]:not([data-active="true"]):has([data-titlebar-tab]:hover) + [data-titlebar-tab-slot]::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-titlebar-tab][data-title-overflow="true"]:not([data-editing="true"]) [data-slot="tab-close"]::before {
|
||||
display: block;
|
||||
right: 0;
|
||||
[data-titlebar-tab][data-title-overflow="true"]:not([data-editing="true"]) [data-slot="tab-link"] {
|
||||
--tab-title-fade-offset: 4px;
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to right,
|
||||
black 0,
|
||||
black calc(100% - var(--tab-title-fade-offset) - 16px),
|
||||
transparent calc(100% - var(--tab-title-fade-offset)),
|
||||
transparent 100%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
black 0,
|
||||
black calc(100% - var(--tab-title-fade-offset) - 16px),
|
||||
transparent calc(100% - var(--tab-title-fade-offset)),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
[data-titlebar-tab][data-title-overflow="true"]:hover:not([data-editing="true"]) [data-slot="tab-close"]::before,
|
||||
[data-titlebar-tab][data-title-overflow="true"][data-active="true"]:not([data-editing="true"])
|
||||
[data-slot="tab-close"]::before {
|
||||
right: 100%;
|
||||
[data-titlebar-tab][data-title-overflow="true"]:is(:hover, [data-active="true"]):not([data-editing="true"])
|
||||
[data-slot="tab-link"] {
|
||||
--tab-title-fade-offset: 24px;
|
||||
}
|
||||
|
||||
[data-titlebar-tab][data-title-overflow="true"]:not(:hover):not([data-active="true"]):not([data-editing="true"])
|
||||
|
||||
@@ -180,11 +180,11 @@ export function TabNavItem(props: {
|
||||
data-slot="titlebar-tab-item"
|
||||
data-title-overflow={titleOverflowing()}
|
||||
data-editing={editing()}
|
||||
class="group relative flex h-7 w-full min-w-0 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
||||
class="group relative flex h-7 w-full min-w-0 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] px-1.5 [container-type:inline-size]"
|
||||
classList={{ invisible: props.hidden }}
|
||||
data-active={props.active}
|
||||
data-dragging={props.dragging}
|
||||
data-pressed={props.pressed}
|
||||
data-state={props.active || props.pressed ? "pressed" : undefined}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button !== MIDDLE_MOUSE_BUTTON) return
|
||||
event.preventDefault()
|
||||
@@ -276,7 +276,7 @@ export function TabNavItem(props: {
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div data-slot="tab-close" class="group-hover:bg-[var(--tab-bg)] group-data-[active=true]:bg-[var(--tab-bg)]">
|
||||
<div data-slot="tab-close">
|
||||
<IconButtonV2
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
@@ -334,8 +334,8 @@ export function DraftTabItem(props: {
|
||||
data-slot="titlebar-tab-item"
|
||||
data-active={props.active}
|
||||
data-dragging={props.dragging}
|
||||
data-pressed={props.pressed}
|
||||
class="group relative flex h-7 w-full min-w-0 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
||||
data-state={props.active || props.pressed ? "pressed" : undefined}
|
||||
class="group relative flex h-7 w-full min-w-0 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] px-1.5 [container-type:inline-size] whitespace-nowrap"
|
||||
classList={{ invisible: props.hidden }}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button !== MIDDLE_MOUSE_BUTTON) return
|
||||
@@ -381,7 +381,7 @@ export function DraftTabItem(props: {
|
||||
{props.title}
|
||||
</span>
|
||||
</a>
|
||||
<div data-slot="tab-close" class="group-hover:bg-[var(--tab-bg)] group-data-[active=true]:bg-[var(--tab-bg)]">
|
||||
<div data-slot="tab-close">
|
||||
<IconButtonV2
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
|
||||
@@ -395,27 +395,25 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
||||
}}
|
||||
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
||||
/>
|
||||
<Show when={!creating()}>
|
||||
<TooltipV2
|
||||
placement="bottom"
|
||||
value={
|
||||
<>
|
||||
{language.t("command.session.new")}
|
||||
<KeybindV2 keys={newTabTooltipKeybind(command)} variant="neutral" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<IconButtonV2
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="shrink-0"
|
||||
icon={<IconV2 name="plus" />}
|
||||
onClick={openNewTab}
|
||||
aria-label={language.t("command.session.new")}
|
||||
/>
|
||||
</TooltipV2>
|
||||
</Show>
|
||||
<TooltipV2
|
||||
placement="bottom"
|
||||
value={
|
||||
<>
|
||||
{language.t("command.session.new")}
|
||||
<KeybindV2 keys={newTabTooltipKeybind(command)} variant="neutral" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<IconButtonV2
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="shrink-0"
|
||||
icon={<IconV2 name="plus" />}
|
||||
onClick={openNewTab}
|
||||
aria-label={language.t("command.session.new")}
|
||||
/>
|
||||
</TooltipV2>
|
||||
<div class="flex-1" />
|
||||
<TitlebarV2Right state={v2RightState()} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ServerConnection } from "./server"
|
||||
import type { Tab } from "./tabs"
|
||||
|
||||
export function migrateTabs(value: unknown, fallback: ServerConnection.Key): Tab[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.flatMap<Tab>((tab) => {
|
||||
if (!tab || typeof tab !== "object") return []
|
||||
if ("server" in tab && typeof tab.server !== "string") return []
|
||||
const server = ("server" in tab ? tab.server : fallback) as ServerConnection.Key
|
||||
if (tab.type === "session" && typeof tab.sessionId === "string") {
|
||||
return [{ type: tab.type, server, sessionId: tab.sessionId }]
|
||||
}
|
||||
if (
|
||||
tab.type === "draft" &&
|
||||
typeof tab.draftID === "string" &&
|
||||
typeof tab.directory === "string" &&
|
||||
(tab.worktree === undefined || typeof tab.worktree === "string")
|
||||
) {
|
||||
return [{ type: tab.type, server, draftID: tab.draftID, directory: tab.directory, worktree: tab.worktree }]
|
||||
}
|
||||
return []
|
||||
})
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { createRoot, getOwner, onCleanup } from "solid-js"
|
||||
import { createTabMemory } from "./tab-memory"
|
||||
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
|
||||
import type { SessionTab, Tab } from "./tabs"
|
||||
import { migrateTabs } from "./tab-migration"
|
||||
import type { ServerConnection } from "./server"
|
||||
|
||||
const server = "local\nhttp://localhost:4096" as ServerConnection.Key
|
||||
@@ -11,6 +12,23 @@ function sessionTab(sessionId: string): SessionTab {
|
||||
return { type: "session", server, sessionId }
|
||||
}
|
||||
|
||||
describe("tab migration", () => {
|
||||
test("drops null and malformed persisted tabs", () => {
|
||||
expect(
|
||||
migrateTabs([null, sessionTab("a"), { type: "session", server }, { type: "unknown", server }, "invalid"], server),
|
||||
).toEqual([sessionTab("a")])
|
||||
})
|
||||
|
||||
test("adds the fallback server to valid legacy tabs", () => {
|
||||
expect(migrateTabs([{ type: "session", sessionId: "a", dirBase64: "legacy" }], server)).toEqual([sessionTab("a")])
|
||||
})
|
||||
|
||||
test("replaces invalid top-level persisted data", () => {
|
||||
expect(migrateTabs(null, server)).toEqual([])
|
||||
expect(migrateTabs({}, server)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("tab memory", () => {
|
||||
test("keeps state until its tab is removed", () => {
|
||||
createRoot((dispose) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { sessionHref } from "@/utils/session-route"
|
||||
import { createTabMemory } from "./tab-memory"
|
||||
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
|
||||
import { createDraftPromptSession, type PromptModel } from "./prompt-state"
|
||||
import { migrateTabs } from "./tab-migration"
|
||||
|
||||
export type SessionTab = {
|
||||
type: "session"
|
||||
@@ -59,13 +60,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
{
|
||||
...Persist.window("tabs"),
|
||||
migrate: (value: unknown) => {
|
||||
if (!Array.isArray(value)) return value
|
||||
return value.map((tab) => {
|
||||
if (!tab || typeof tab !== "object" || "server" in tab) return tab
|
||||
return { ...tab, server: fallback }
|
||||
})
|
||||
},
|
||||
migrate: (value: unknown) => migrateTabs(value, fallback),
|
||||
},
|
||||
createStore<Tab[]>([]),
|
||||
)
|
||||
|
||||
@@ -277,6 +277,7 @@ export const dict = {
|
||||
"prompt.action.send": "إرسال",
|
||||
"prompt.action.stop": "توقف",
|
||||
"prompt.toast.pasteUnsupported.title": "مرفق غير مدعوم",
|
||||
"prompt.toast.attachmentDuplicate.title": "تم تحميل هذا الملف بالفعل",
|
||||
"prompt.toast.pasteUnsupported.description": "يمكن إرفاق الصور أو ملفات PDF أو الملفات النصية فقط هنا.",
|
||||
"prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً",
|
||||
"prompt.toast.modelAgentRequired.description": "اختر وكيلاً ونموذجاً قبل إرسال الموجه.",
|
||||
|
||||
@@ -277,6 +277,7 @@ export const dict = {
|
||||
"prompt.action.send": "Enviar",
|
||||
"prompt.action.stop": "Parar",
|
||||
"prompt.toast.pasteUnsupported.title": "Anexo não suportado",
|
||||
"prompt.toast.attachmentDuplicate.title": "Este arquivo já foi enviado",
|
||||
"prompt.toast.pasteUnsupported.description": "Apenas imagens, PDFs ou arquivos de texto podem ser anexados aqui.",
|
||||
"prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo",
|
||||
"prompt.toast.modelAgentRequired.description": "Escolha um agente e modelo antes de enviar um prompt.",
|
||||
|
||||
@@ -298,6 +298,7 @@ export const dict = {
|
||||
"prompt.action.stop": "Zaustavi",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Nepodržan prilog",
|
||||
"prompt.toast.attachmentDuplicate.title": "Ova datoteka je već učitana",
|
||||
"prompt.toast.pasteUnsupported.description": "Ovdje se mogu priložiti samo slike, PDF-ovi ili tekstualne datoteke.",
|
||||
"prompt.toast.modelAgentRequired.title": "Odaberi agenta i model",
|
||||
"prompt.toast.modelAgentRequired.description": "Odaberi agenta i model prije slanja upita.",
|
||||
|
||||
@@ -296,6 +296,7 @@ export const dict = {
|
||||
"prompt.action.stop": "Stop",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Ikke understøttet vedhæftning",
|
||||
"prompt.toast.attachmentDuplicate.title": "Denne fil er allerede uploadet",
|
||||
"prompt.toast.pasteUnsupported.description": "Kun billeder, PDF'er eller tekstfiler kan vedhæftes her.",
|
||||
"prompt.toast.modelAgentRequired.title": "Vælg en agent og model",
|
||||
"prompt.toast.modelAgentRequired.description": "Vælg en agent og model før du sender en forespørgsel.",
|
||||
|
||||
@@ -282,6 +282,7 @@ export const dict = {
|
||||
"prompt.action.send": "Senden",
|
||||
"prompt.action.stop": "Stopp",
|
||||
"prompt.toast.pasteUnsupported.title": "Nicht unterstützter Anhang",
|
||||
"prompt.toast.attachmentDuplicate.title": "Diese Datei wurde bereits hochgeladen",
|
||||
"prompt.toast.pasteUnsupported.description": "Hier können nur Bilder, PDFs oder Textdateien angehängt werden.",
|
||||
"prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell",
|
||||
"prompt.toast.modelAgentRequired.description":
|
||||
|
||||
@@ -301,6 +301,7 @@ export const dict = {
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Unsupported attachment",
|
||||
"prompt.toast.pasteUnsupported.description": "Only images, PDFs, or text files can be attached here.",
|
||||
"prompt.toast.attachmentDuplicate.title": "This file has already been uploaded",
|
||||
"prompt.toast.modelAgentRequired.title": "Select an agent and model",
|
||||
"prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.",
|
||||
"prompt.toast.worktreeCreateFailed.title": "Failed to create worktree",
|
||||
|
||||
@@ -297,6 +297,7 @@ export const dict = {
|
||||
"prompt.action.stop": "Detener",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Adjunto no compatible",
|
||||
"prompt.toast.attachmentDuplicate.title": "Este archivo ya se ha subido",
|
||||
"prompt.toast.pasteUnsupported.description": "Solo se pueden adjuntar imágenes, PDFs o archivos de texto aquí.",
|
||||
"prompt.toast.modelAgentRequired.title": "Selecciona un agente y modelo",
|
||||
"prompt.toast.modelAgentRequired.description": "Elige un agente y modelo antes de enviar un prompt.",
|
||||
|
||||
@@ -277,6 +277,7 @@ export const dict = {
|
||||
"prompt.action.send": "Envoyer",
|
||||
"prompt.action.stop": "Arrêter",
|
||||
"prompt.toast.pasteUnsupported.title": "Pièce jointe non prise en charge",
|
||||
"prompt.toast.attachmentDuplicate.title": "Ce fichier a déjà été téléversé",
|
||||
"prompt.toast.pasteUnsupported.description":
|
||||
"Seules les images, les PDF ou les fichiers texte peuvent être joints ici.",
|
||||
"prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle",
|
||||
|
||||
@@ -276,6 +276,7 @@ export const dict = {
|
||||
"prompt.action.send": "送信",
|
||||
"prompt.action.stop": "停止",
|
||||
"prompt.toast.pasteUnsupported.title": "サポートされていない添付ファイル",
|
||||
"prompt.toast.attachmentDuplicate.title": "このファイルはすでにアップロードされています",
|
||||
"prompt.toast.pasteUnsupported.description": "画像、PDF、またはテキストファイルのみ添付できます。",
|
||||
"prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択",
|
||||
"prompt.toast.modelAgentRequired.description": "プロンプトを送信する前にエージェントとモデルを選択してください。",
|
||||
|
||||
@@ -264,6 +264,7 @@ export const dict = {
|
||||
"prompt.action.send": "전송",
|
||||
"prompt.action.stop": "중지",
|
||||
"prompt.toast.pasteUnsupported.title": "지원되지 않는 첨부 파일",
|
||||
"prompt.toast.attachmentDuplicate.title": "이 파일은 이미 업로드되었습니다",
|
||||
"prompt.toast.pasteUnsupported.description": "이미지, PDF 또는 텍스트 파일만 첨부할 수 있습니다.",
|
||||
"prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택",
|
||||
"prompt.toast.modelAgentRequired.description": "프롬프트를 보내기 전에 에이전트와 모델을 선택하세요.",
|
||||
|
||||
@@ -288,6 +288,7 @@ export const dict = {
|
||||
"prompt.action.stop": "Stopp",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Ikke støttet vedlegg",
|
||||
"prompt.toast.attachmentDuplicate.title": "Denne filen er allerede lastet opp",
|
||||
"prompt.toast.pasteUnsupported.description": "Kun bilder, PDF-er eller tekstfiler kan legges ved her.",
|
||||
"prompt.toast.modelAgentRequired.title": "Velg en agent og modell",
|
||||
"prompt.toast.modelAgentRequired.description": "Velg en agent og modell før du sender en forespørsel.",
|
||||
|
||||
@@ -278,6 +278,7 @@ export const dict = {
|
||||
"prompt.action.send": "Wyślij",
|
||||
"prompt.action.stop": "Zatrzymaj",
|
||||
"prompt.toast.pasteUnsupported.title": "Nieobsługiwany załącznik",
|
||||
"prompt.toast.attachmentDuplicate.title": "Ten plik został już przesłany",
|
||||
"prompt.toast.pasteUnsupported.description": "Można tutaj załączać tylko obrazy, pliki PDF lub pliki tekstowe.",
|
||||
"prompt.toast.modelAgentRequired.title": "Wybierz agenta i model",
|
||||
"prompt.toast.modelAgentRequired.description": "Wybierz agenta i model przed wysłaniem zapytania.",
|
||||
|
||||
@@ -297,6 +297,7 @@ export const dict = {
|
||||
"prompt.action.stop": "Остановить",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Неподдерживаемое вложение",
|
||||
"prompt.toast.attachmentDuplicate.title": "Этот файл уже загружен",
|
||||
"prompt.toast.pasteUnsupported.description": "Здесь можно прикрепить только изображения, PDF или текстовые файлы.",
|
||||
"prompt.toast.modelAgentRequired.title": "Выберите агента и модель",
|
||||
"prompt.toast.modelAgentRequired.description": "Выберите агента и модель перед отправкой запроса.",
|
||||
|
||||
@@ -297,6 +297,7 @@ export const dict = {
|
||||
"prompt.action.stop": "หยุด",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "ไฟล์แนบที่ไม่รองรับ",
|
||||
"prompt.toast.attachmentDuplicate.title": "ไฟล์นี้ถูกอัปโหลดแล้ว",
|
||||
"prompt.toast.pasteUnsupported.description": "แนบได้เฉพาะรูปภาพ, PDF หรือไฟล์ข้อความเท่านั้น",
|
||||
"prompt.toast.modelAgentRequired.title": "เลือกเอเจนต์และโมเดล",
|
||||
"prompt.toast.modelAgentRequired.description": "เลือกเอเจนต์และโมเดลก่อนส่งพร้อมท์",
|
||||
|
||||
@@ -302,6 +302,7 @@ export const dict = {
|
||||
"prompt.action.stop": "Durdur",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Desteklenmeyen ek",
|
||||
"prompt.toast.attachmentDuplicate.title": "Bu dosya zaten yüklendi",
|
||||
"prompt.toast.pasteUnsupported.description": "Buraya yalnızca resimler, PDF'ler veya metin dosyaları eklenebilir.",
|
||||
"prompt.toast.modelAgentRequired.title": "Bir ajan ve model seçin",
|
||||
"prompt.toast.modelAgentRequired.description": "Komut göndermeden önce bir ajan ve model seçin.",
|
||||
|
||||
@@ -299,6 +299,7 @@ export const dict = {
|
||||
"prompt.action.stop": "Зупинити",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "Непідтримуване вкладення",
|
||||
"prompt.toast.attachmentDuplicate.title": "Цей файл уже завантажено",
|
||||
"prompt.toast.pasteUnsupported.description": "Сюди можна прикріплювати лише зображення, PDF або текстові файли.",
|
||||
"prompt.toast.modelAgentRequired.title": "Виберіть агента та модель",
|
||||
"prompt.toast.modelAgentRequired.description": "Виберіть агента та модель перед надсиланням запиту.",
|
||||
|
||||
@@ -316,6 +316,7 @@ export const dict = {
|
||||
"prompt.action.send": "发送",
|
||||
"prompt.action.stop": "停止",
|
||||
"prompt.toast.pasteUnsupported.title": "不支持的附件",
|
||||
"prompt.toast.attachmentDuplicate.title": "此文件已上传",
|
||||
"prompt.toast.pasteUnsupported.description": "此处仅能附加图片、PDF 或文本文件。",
|
||||
"prompt.toast.modelAgentRequired.title": "请选择智能体和模型",
|
||||
"prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。",
|
||||
|
||||
@@ -297,6 +297,7 @@ export const dict = {
|
||||
"prompt.action.stop": "停止",
|
||||
|
||||
"prompt.toast.pasteUnsupported.title": "不支援的附件",
|
||||
"prompt.toast.attachmentDuplicate.title": "此檔案已上傳",
|
||||
"prompt.toast.pasteUnsupported.description": "此處僅能附加圖片、PDF 或文字檔案。",
|
||||
"prompt.toast.modelAgentRequired.title": "請選擇代理程式和模型",
|
||||
"prompt.toast.modelAgentRequired.description": "傳送提示前請先選擇代理程式和模型。",
|
||||
|
||||
@@ -33,8 +33,7 @@ import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { toaster } from "@opencode-ai/ui/toast"
|
||||
import { setV2Toast, showToast, ToastRegion } from "@/utils/toast"
|
||||
import { dismissToast, setV2Toast, showToast, ToastRegion } from "@/utils/toast"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { normalizeProjectInfo } from "@/context/global-sync/utils"
|
||||
import { clearWorkspaceTerminals } from "@/context/terminal"
|
||||
@@ -384,7 +383,7 @@ export default function LegacyLayout(props: ParentProps) {
|
||||
const dismissSessionAlert = (sessionKey: string) => {
|
||||
const toastId = toastBySession.get(sessionKey)
|
||||
if (toastId === undefined) return
|
||||
toaster.dismiss(toastId)
|
||||
dismissToast(toastId)
|
||||
toastBySession.delete(sessionKey)
|
||||
alertedAtBySession.delete(sessionKey)
|
||||
}
|
||||
@@ -1459,7 +1458,7 @@ export default function LegacyLayout(props: ParentProps) {
|
||||
title: language.t("workspace.resetting.title"),
|
||||
description: language.t("workspace.resetting.description"),
|
||||
})
|
||||
const dismiss = () => toaster.dismiss(progress)
|
||||
const dismiss = () => dismissToast(progress)
|
||||
|
||||
const sessions = await listAllSessions(serverSDK().api.session, { directory, order: "desc" }).catch(() => [])
|
||||
|
||||
@@ -2450,7 +2449,7 @@ function UpdateAvailableToast(props: {
|
||||
|
||||
onCleanup(() => {
|
||||
if (toastId === undefined) return
|
||||
toaster.dismiss(toastId)
|
||||
dismissToast(toastId)
|
||||
})
|
||||
|
||||
return null
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Icon, type IconProps } from "@opencode-ai/ui/icon"
|
||||
import { Toast, showToast as showLegacyToast, type ToastOptions, type ToastVariant } from "@opencode-ai/ui/toast"
|
||||
import { ToastV2, showToastV2 } from "@opencode-ai/ui/v2/toast-v2"
|
||||
import {
|
||||
Toast,
|
||||
showToast as showLegacyToast,
|
||||
toaster as legacyToaster,
|
||||
type ToastOptions,
|
||||
type ToastVariant,
|
||||
} from "@opencode-ai/ui/toast"
|
||||
import { ToastV2, showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2"
|
||||
|
||||
let v2 = false
|
||||
|
||||
@@ -27,6 +33,13 @@ export function showToast(options: ToastOptions | string) {
|
||||
})
|
||||
}
|
||||
|
||||
// v1 and v2 ids come from separate registries, so dismissal has to use the same
|
||||
// implementation that issued the id.
|
||||
export function dismissToast(toastId: number) {
|
||||
if (!v2) return legacyToaster.dismiss(toastId)
|
||||
return toasterV2.dismiss(toastId)
|
||||
}
|
||||
|
||||
function resolveIcon(icon: IconProps["name"] | undefined, variant: ToastVariant | undefined) {
|
||||
const name = icon ?? (variant === "success" ? "check" : undefined)
|
||||
if (!name) return
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createRoot } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createPromptAttachmentsCore } from "@/components/prompt-input/attachments"
|
||||
import { createPromptState } from "@/context/prompt"
|
||||
import { createPromptInputV2Attachments } from "../../session-ui/src/v2/components/prompt-input/attachments"
|
||||
import type { PromptInputV2Prompt } from "../../session-ui/src/v2/components/prompt-input/types"
|
||||
|
||||
describe("prompt attachment session ownership", () => {
|
||||
test("adds an asynchronously read image to the session where the read started", async () => {
|
||||
@@ -85,6 +88,88 @@ describe("prompt attachment session ownership", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("rejects a duplicate native clipboard attachment in the V2 prompt store", async () => {
|
||||
await createRoot(async (dispose) => {
|
||||
const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt })
|
||||
const duplicate = Promise.withResolvers<void>()
|
||||
const files = [
|
||||
new File(["hello"], "clipboard-1.txt", { type: "text/plain" }),
|
||||
new File(["hello"], "clipboard-2.txt", { type: "text/plain" }),
|
||||
]
|
||||
const attachments = createPromptInputV2Attachments({
|
||||
capture: () => ({
|
||||
current: () => state.prompt,
|
||||
cursor: () => 0,
|
||||
set: (prompt) => setState("prompt", prompt),
|
||||
}),
|
||||
editor: () => document.createElement("div"),
|
||||
focusEditor: () => undefined,
|
||||
addPart: () => false,
|
||||
setDraggingType: () => undefined,
|
||||
directory: () => "/",
|
||||
isDialogActive: () => false,
|
||||
warn: () => undefined,
|
||||
duplicate: duplicate.resolve,
|
||||
onError: () => undefined,
|
||||
readClipboardImage: async () => files.shift() ?? null,
|
||||
})
|
||||
const event = {
|
||||
clipboardData: { items: [], getData: () => "" },
|
||||
preventDefault: () => undefined,
|
||||
stopPropagation: () => undefined,
|
||||
} as unknown as ClipboardEvent
|
||||
|
||||
await attachments.handlePaste(event)
|
||||
await attachments.handlePaste(event)
|
||||
await duplicate.promise
|
||||
|
||||
expect(state.prompt).toHaveLength(1)
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("rejects desktop duplicates and keeps changed files in the V2 prompt store", async () => {
|
||||
await createRoot(async (dispose) => {
|
||||
const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt })
|
||||
const duplicates: string[] = []
|
||||
const attachments = createPromptInputV2Attachments({
|
||||
capture: () => ({
|
||||
current: () => state.prompt,
|
||||
cursor: () => 0,
|
||||
set: (prompt) => setState("prompt", prompt),
|
||||
}),
|
||||
editor: () => document.createElement("div"),
|
||||
focusEditor: () => undefined,
|
||||
addPart: () => false,
|
||||
setDraggingType: () => undefined,
|
||||
directory: () => "/",
|
||||
isDialogActive: () => false,
|
||||
warn: () => undefined,
|
||||
duplicate: () => duplicates.push("duplicate"),
|
||||
onError: () => undefined,
|
||||
getPathForFile: (file) => (file.name === "browser.txt" ? "" : `/tmp/${file.name}`),
|
||||
})
|
||||
const first = new File(["first"], "a.txt", { type: "text/plain" })
|
||||
const second = new File(["second"], "b.txt", { type: "text/plain" })
|
||||
|
||||
await attachments.addAttachments([first, second])
|
||||
await attachments.addAttachments([first, second])
|
||||
expect(state.prompt).toHaveLength(2)
|
||||
expect(duplicates).toEqual(["duplicate", "duplicate"])
|
||||
|
||||
await attachments.addAttachments([new File(["edited"], "a.txt", { type: "text/plain" })])
|
||||
expect(state.prompt).toHaveLength(3)
|
||||
|
||||
await attachments.addAttachments([
|
||||
new File(["same"], "browser.txt", { type: "text/plain" }),
|
||||
new File(["same"], "browser.txt", { type: "text/plain" }),
|
||||
])
|
||||
expect(state.prompt).toHaveLength(4)
|
||||
expect(duplicates).toHaveLength(3)
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
function images(prompt: ReturnType<typeof createPromptState>) {
|
||||
return prompt.current().filter((part) => part.type === "image")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,46 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { beforeEach, describe, expect, test } from "bun:test"
|
||||
import { createSignal, type JSX } from "solid-js"
|
||||
import { showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2"
|
||||
|
||||
describe("showToastV2", () => {
|
||||
// The toast registry is module state, so each test starts from an empty stack.
|
||||
beforeEach(() => {
|
||||
toasterV2.dismiss()
|
||||
})
|
||||
|
||||
test("coalesces exact active content", () => {
|
||||
const first = showToastV2({ title: "Repeated error", description: "Try again" })
|
||||
const second = showToastV2({ title: "Repeated error", description: "Try again" })
|
||||
const different = showToastV2({ title: "Repeated error", description: "A different error" })
|
||||
|
||||
expect(second).toBe(first)
|
||||
expect(different).not.toBe(first)
|
||||
|
||||
toasterV2.dismiss(first)
|
||||
toasterV2.dismiss(different)
|
||||
})
|
||||
|
||||
test("allows dismissed content to appear again", () => {
|
||||
const first = showToastV2("Dismiss and retry")
|
||||
toasterV2.dismiss(first)
|
||||
|
||||
const second = showToastV2("Dismiss and retry")
|
||||
expect(second).not.toBe(first)
|
||||
|
||||
toasterV2.dismiss(second)
|
||||
})
|
||||
|
||||
test("recreates matching content when it is not the topmost toast", () => {
|
||||
const first = showToastV2("First toast")
|
||||
const topmost = showToastV2("Topmost toast")
|
||||
const repeated = showToastV2("First toast")
|
||||
|
||||
expect(repeated).not.toBe(first)
|
||||
|
||||
toasterV2.dismiss(topmost)
|
||||
toasterV2.dismiss(repeated)
|
||||
})
|
||||
|
||||
test("creates no reactive computations at call time", () => {
|
||||
const [tick, setTick] = createSignal(0)
|
||||
let reads = 0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
|
||||
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.opencode.ai"
|
||||
|
||||
export const modelsData = process.env.MODELS_DEV_API_JSON
|
||||
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -108,7 +108,7 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "6.12.0",
|
||||
"gitlab-ai-provider": "6.12.1",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
|
||||
@@ -151,10 +151,10 @@ const layer = Layer.effect(
|
||||
),
|
||||
)
|
||||
|
||||
const source = Flag.OPENCODE_MODELS_URL || "https://models.dev"
|
||||
const source = Flag.OPENCODE_MODELS_URL || "https://models.opencode.ai"
|
||||
const filepath = path.join(
|
||||
Global.Path.cache,
|
||||
source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
|
||||
source === "https://models.opencode.ai" ? "models.json" : `models-${Hash.fast(source)}.json`,
|
||||
)
|
||||
const ttl = Duration.minutes(5)
|
||||
const lockKey = `models-dev:${filepath}`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises"
|
||||
import * as path from "node:path"
|
||||
|
||||
const RECORDINGS_DIR = path.resolve(import.meta.dir, "..", "test", "fixtures", "recordings")
|
||||
const MODELS_DEV_URL = "https://models.dev/api.json"
|
||||
const MODELS_DEV_URL = "https://models.opencode.ai/api.json"
|
||||
|
||||
type JsonRecord = Record<string, unknown>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -120,7 +120,7 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "6.12.0",
|
||||
"gitlab-ai-provider": "6.12.1",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
|
||||
@@ -13,6 +13,7 @@ import { CodexAuthPlugin } from "./openai/codex"
|
||||
import { Session } from "@/session/session"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { CopilotAuthPlugin } from "./github-copilot/copilot"
|
||||
import { ModalPlugin } from "./modal/modal"
|
||||
import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"
|
||||
import { PoeAuthPlugin } from "opencode-poe-auth"
|
||||
import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare"
|
||||
@@ -70,6 +71,7 @@ function internalPlugins(flags: RuntimeFlags.Info): PluginInstance[] {
|
||||
experimentalWebSockets: experimentalWebSocketsEnabled({ enabled: flags.experimentalWebSockets }),
|
||||
}),
|
||||
CopilotAuthPlugin,
|
||||
ModalPlugin,
|
||||
GitlabAuthPlugin,
|
||||
PoeAuthPlugin,
|
||||
CloudflareWorkersAuthPlugin,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Hooks } from "@opencode-ai/plugin"
|
||||
import { ModalModels } from "./models"
|
||||
|
||||
export async function ModalPlugin(): Promise<Hooks> {
|
||||
return {
|
||||
provider: {
|
||||
id: "modal",
|
||||
async models(provider, ctx) {
|
||||
const apiKey = ctx.auth?.type === "api" ? ctx.auth.key : process.env.MODAL_PROXY_TOKEN
|
||||
const baseURL = Object.values(provider.models)[0]?.api.url
|
||||
if (!apiKey || !baseURL) return {}
|
||||
|
||||
return ModalModels.get(baseURL, apiKey, provider.models).catch(() => ({}))
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import type { Model } from "@opencode-ai/sdk/v2"
|
||||
import { Schema } from "effect"
|
||||
|
||||
const reasoningOption = Schema.Struct({
|
||||
type: Schema.Literal("effort"),
|
||||
values: Schema.Array(Schema.NullOr(Schema.String)),
|
||||
})
|
||||
|
||||
const response = Schema.Struct({
|
||||
data: Schema.Array(
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
base_model_id: Schema.optional(Schema.String),
|
||||
hugging_face_id: Schema.optional(Schema.String),
|
||||
name: Schema.optional(Schema.String),
|
||||
input_modalities: Schema.optional(Schema.Array(Schema.String)),
|
||||
output_modalities: Schema.optional(Schema.Array(Schema.String)),
|
||||
context_length: Schema.optional(Schema.Number),
|
||||
max_output_length: Schema.optional(Schema.Number),
|
||||
pricing: Schema.optional(
|
||||
Schema.Struct({
|
||||
prompt: Schema.optional(Schema.Union([Schema.String, Schema.Number])),
|
||||
completion: Schema.optional(Schema.Union([Schema.String, Schema.Number])),
|
||||
input_cache_read: Schema.optional(Schema.Union([Schema.String, Schema.Number])),
|
||||
}),
|
||||
),
|
||||
supported_sampling_parameters: Schema.optional(Schema.Array(Schema.String)),
|
||||
supported_features: Schema.optional(Schema.Array(Schema.String)),
|
||||
reasoning_options: Schema.optional(Schema.Array(reasoningOption)),
|
||||
interleaved: Schema.optional(
|
||||
Schema.Union([
|
||||
Schema.Boolean,
|
||||
Schema.Struct({
|
||||
field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
const decode = Schema.decodeUnknownSync(response)
|
||||
|
||||
function price(value: string | number | undefined, fallback: number) {
|
||||
if (value === undefined) return fallback
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed * 1_000_000 : fallback
|
||||
}
|
||||
|
||||
export async function get(baseURL: string, apiKey: string, existing: Record<string, Model>) {
|
||||
const data = await fetch(`${baseURL.replace(/\/+$/, "")}/models`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
signal: AbortSignal.timeout(3_000),
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch Modal models: ${res.status}`)
|
||||
return decode(await res.json())
|
||||
})
|
||||
|
||||
return Object.fromEntries(
|
||||
data.data.map((item) => {
|
||||
const template = existing[item.base_model_id ?? item.hugging_face_id ?? item.id]
|
||||
const model: Model = {
|
||||
id: item.id,
|
||||
providerID: "modal",
|
||||
name: item.name ?? template?.name ?? item.id,
|
||||
family: template?.family,
|
||||
api: {
|
||||
id: item.id,
|
||||
url: baseURL,
|
||||
npm: template?.api.npm ?? "@ai-sdk/openai-compatible",
|
||||
},
|
||||
status: template?.status ?? "active",
|
||||
headers: { ...template?.headers },
|
||||
options: { ...template?.options },
|
||||
cost: {
|
||||
input: price(item.pricing?.prompt, template?.cost.input ?? 0),
|
||||
output: price(item.pricing?.completion, template?.cost.output ?? 0),
|
||||
cache: {
|
||||
read: price(item.pricing?.input_cache_read, template?.cost.cache.read ?? 0),
|
||||
write: template?.cost.cache.write ?? 0,
|
||||
},
|
||||
},
|
||||
limit: {
|
||||
context: item.context_length ?? template?.limit.context ?? 0,
|
||||
input: template?.limit.input,
|
||||
output: item.max_output_length ?? template?.limit.output ?? 0,
|
||||
},
|
||||
capabilities: {
|
||||
temperature:
|
||||
item.supported_sampling_parameters?.includes("temperature") ?? template?.capabilities.temperature ?? false,
|
||||
reasoning: item.supported_features?.includes("reasoning") ?? template?.capabilities.reasoning ?? false,
|
||||
attachment:
|
||||
item.input_modalities?.some((modality) => modality !== "text") ??
|
||||
template?.capabilities.attachment ??
|
||||
false,
|
||||
toolcall: item.supported_features?.includes("tools") ?? template?.capabilities.toolcall ?? true,
|
||||
input: {
|
||||
text: item.input_modalities?.includes("text") ?? template?.capabilities.input.text ?? true,
|
||||
audio: item.input_modalities?.includes("audio") ?? template?.capabilities.input.audio ?? false,
|
||||
image: item.input_modalities?.includes("image") ?? template?.capabilities.input.image ?? false,
|
||||
video: item.input_modalities?.includes("video") ?? template?.capabilities.input.video ?? false,
|
||||
pdf: item.input_modalities?.includes("pdf") ?? template?.capabilities.input.pdf ?? false,
|
||||
},
|
||||
output: {
|
||||
text: item.output_modalities?.includes("text") ?? template?.capabilities.output.text ?? true,
|
||||
audio: item.output_modalities?.includes("audio") ?? template?.capabilities.output.audio ?? false,
|
||||
image: item.output_modalities?.includes("image") ?? template?.capabilities.output.image ?? false,
|
||||
video: item.output_modalities?.includes("video") ?? template?.capabilities.output.video ?? false,
|
||||
pdf: item.output_modalities?.includes("pdf") ?? template?.capabilities.output.pdf ?? false,
|
||||
},
|
||||
interleaved: item.interleaved ?? template?.capabilities.interleaved ?? false,
|
||||
},
|
||||
release_date: template?.release_date ?? "",
|
||||
}
|
||||
model.variants =
|
||||
item.reasoning_options === undefined
|
||||
? template?.variants
|
||||
: Object.fromEntries(
|
||||
item.reasoning_options.flatMap((option) =>
|
||||
option.values.map((value) => {
|
||||
const effort = value ?? "none"
|
||||
return [effort, { reasoningEffort: effort }]
|
||||
}),
|
||||
),
|
||||
)
|
||||
return [item.id, model]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export * as ModalModels from "./models"
|
||||
@@ -0,0 +1,198 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import type { Model, Provider } from "@opencode-ai/sdk/v2"
|
||||
import { ModalPlugin } from "@/plugin/modal/modal"
|
||||
|
||||
const BASE_MODEL_ID = "thinkingmachines/Inkling-NVFP4"
|
||||
const RUNTIME_MODEL_ID = "workspace--inkling.us-west.modal.direct"
|
||||
const FALLBACK_RUNTIME_MODEL_ID = "workspace--inkling-fallback.us-west.modal.direct"
|
||||
|
||||
function makeProvider(baseURL: string): Provider {
|
||||
const template: Model = {
|
||||
id: BASE_MODEL_ID,
|
||||
providerID: "modal",
|
||||
name: "Inkling",
|
||||
family: "ling",
|
||||
api: {
|
||||
id: BASE_MODEL_ID,
|
||||
url: baseURL,
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
},
|
||||
status: "active",
|
||||
headers: {},
|
||||
options: {},
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 4,
|
||||
cache: {
|
||||
read: 0.2,
|
||||
write: 0,
|
||||
},
|
||||
},
|
||||
limit: {
|
||||
context: 128_000,
|
||||
output: 8_192,
|
||||
},
|
||||
capabilities: {
|
||||
temperature: true,
|
||||
reasoning: true,
|
||||
attachment: true,
|
||||
toolcall: true,
|
||||
input: {
|
||||
text: true,
|
||||
audio: true,
|
||||
image: true,
|
||||
video: false,
|
||||
pdf: false,
|
||||
},
|
||||
output: {
|
||||
text: true,
|
||||
audio: false,
|
||||
image: false,
|
||||
video: false,
|
||||
pdf: false,
|
||||
},
|
||||
interleaved: {
|
||||
field: "reasoning_content",
|
||||
},
|
||||
},
|
||||
release_date: "2026-07-15",
|
||||
variants: {
|
||||
fallback: {
|
||||
reasoningEffort: "fallback",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
id: "modal",
|
||||
name: "Modal",
|
||||
source: "api",
|
||||
env: ["MODAL_PROXY_TOKEN"],
|
||||
options: {},
|
||||
models: {
|
||||
[template.id]: template,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test("discovers Modal workspace models", async () => {
|
||||
const requests: Array<{ authorization: string | null; path: string }> = []
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(request) {
|
||||
requests.push({
|
||||
authorization: request.headers.get("authorization"),
|
||||
path: new URL(request.url).pathname,
|
||||
})
|
||||
return Response.json({
|
||||
data: [
|
||||
{
|
||||
id: RUNTIME_MODEL_ID,
|
||||
base_model_id: BASE_MODEL_ID,
|
||||
name: "Thinking Machines: Inkling",
|
||||
input_modalities: ["text", "image", "audio"],
|
||||
output_modalities: ["text"],
|
||||
context_length: 1_048_576,
|
||||
max_output_length: 262_144,
|
||||
pricing: {
|
||||
prompt: "0.0000012",
|
||||
completion: "0.000005",
|
||||
input_cache_read: "0.00000027",
|
||||
},
|
||||
supported_sampling_parameters: ["temperature"],
|
||||
supported_features: ["tools", "reasoning"],
|
||||
reasoning_options: [
|
||||
{
|
||||
type: "effort",
|
||||
values: ["none", "low", "medium", "high", "xhigh", "max"],
|
||||
},
|
||||
],
|
||||
interleaved: {
|
||||
field: "reasoning_content",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: FALLBACK_RUNTIME_MODEL_ID,
|
||||
base_model_id: BASE_MODEL_ID,
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
})
|
||||
const provider = makeProvider(`${server.url}v1`)
|
||||
|
||||
const plugin = await ModalPlugin()
|
||||
const models = await plugin.provider!.models!(provider, {
|
||||
auth: {
|
||||
type: "api",
|
||||
key: "test-token",
|
||||
},
|
||||
})
|
||||
const model = models[RUNTIME_MODEL_ID]
|
||||
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
authorization: "Bearer test-token",
|
||||
path: "/v1/models",
|
||||
},
|
||||
])
|
||||
expect(Object.keys(models)).toEqual([RUNTIME_MODEL_ID, FALLBACK_RUNTIME_MODEL_ID])
|
||||
expect(model.api).toEqual({
|
||||
id: RUNTIME_MODEL_ID,
|
||||
url: `${server.url}v1`,
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
})
|
||||
expect(model.family).toBe("ling")
|
||||
expect(model.capabilities.interleaved).toEqual({ field: "reasoning_content" })
|
||||
expect(model.capabilities.input).toEqual({
|
||||
text: true,
|
||||
audio: true,
|
||||
image: true,
|
||||
video: false,
|
||||
pdf: false,
|
||||
})
|
||||
expect(model.cost).toEqual({
|
||||
input: 1.2,
|
||||
output: 5,
|
||||
cache: {
|
||||
read: 0.27,
|
||||
write: 0,
|
||||
},
|
||||
})
|
||||
expect(model.limit).toEqual({
|
||||
context: 1_048_576,
|
||||
output: 262_144,
|
||||
})
|
||||
expect(model.variants).toEqual({
|
||||
none: { reasoningEffort: "none" },
|
||||
low: { reasoningEffort: "low" },
|
||||
medium: { reasoningEffort: "medium" },
|
||||
high: { reasoningEffort: "high" },
|
||||
xhigh: { reasoningEffort: "xhigh" },
|
||||
max: { reasoningEffort: "max" },
|
||||
})
|
||||
expect(models[FALLBACK_RUNTIME_MODEL_ID].variants).toEqual({
|
||||
fallback: {
|
||||
reasoningEffort: "fallback",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("hides Modal models when discovery fails", async () => {
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response(null, { status: 503 })
|
||||
},
|
||||
})
|
||||
|
||||
const plugin = await ModalPlugin()
|
||||
const models = await plugin.provider!.models!(makeProvider(`${server.url}v1`), {
|
||||
auth: {
|
||||
type: "api",
|
||||
key: "test-token",
|
||||
},
|
||||
})
|
||||
|
||||
expect(models).toEqual({})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
@keyframes prompt-input-v2-attachments-fade-left {
|
||||
from {
|
||||
visibility: hidden;
|
||||
}
|
||||
to {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes prompt-input-v2-attachments-fade-right {
|
||||
from {
|
||||
visibility: visible;
|
||||
}
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="prompt-input-v2-attachments"] {
|
||||
timeline-scope: --prompt-input-v2-attachments-scroll;
|
||||
|
||||
[data-slot^="prompt-attachments-fade-"] {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (animation-timeline: --prompt-input-v2-attachments-scroll) and
|
||||
(timeline-scope: --prompt-input-v2-attachments-scroll) {
|
||||
[data-component="prompt-input-v2-attachments"] [data-slot="prompt-attachments-scroll"] {
|
||||
scroll-timeline: --prompt-input-v2-attachments-scroll x;
|
||||
}
|
||||
|
||||
[data-component="prompt-input-v2-attachments"] [data-slot="prompt-attachments-fade-left"] {
|
||||
animation: prompt-input-v2-attachments-fade-left linear both;
|
||||
animation-timeline: --prompt-input-v2-attachments-scroll;
|
||||
animation-range: 0 0.1px;
|
||||
}
|
||||
|
||||
[data-component="prompt-input-v2-attachments"] [data-slot="prompt-attachments-fade-right"] {
|
||||
animation: prompt-input-v2-attachments-fade-right linear both;
|
||||
animation-timeline: --prompt-input-v2-attachments-scroll;
|
||||
animation-range: calc(100% - 1.1px) calc(100% - 1px);
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@ export type PromptInputV2AttachmentConfig = {
|
||||
directory: () => string
|
||||
isDialogActive: () => boolean
|
||||
warn: () => void
|
||||
duplicate: () => void
|
||||
onError: (error: unknown) => void
|
||||
readClipboardImage?: () => Promise<File | null>
|
||||
getPathForFile?: (file: File) => string
|
||||
@@ -94,7 +95,7 @@ export function createPromptInputV2Attachments(
|
||||
if (!editor) return
|
||||
return { prompt, cursor: prompt.cursor() ?? cursorPosition(editor) }
|
||||
}
|
||||
const add = async (file: File, toast = true, target = capture()) => {
|
||||
const add = async (file: File, toast = true, target = capture(), clipboard = false) => {
|
||||
if (!target) return false
|
||||
const mime = await attachmentMime(file)
|
||||
if (!mime) {
|
||||
@@ -103,11 +104,28 @@ export function createPromptInputV2Attachments(
|
||||
}
|
||||
const url = await dataUrl(file, mime)
|
||||
if (!url) return false
|
||||
const sourcePath = input.getPathForFile?.(file) || undefined
|
||||
// Native clipboard images arrive with a fresh timestamped filename on every paste, so identical
|
||||
// clipboard content is matched on bytes alone.
|
||||
const duplicate = target.prompt
|
||||
.current()
|
||||
.some(
|
||||
(part) =>
|
||||
part.type === "image" &&
|
||||
part.dataUrl === url &&
|
||||
(sourcePath
|
||||
? part.sourcePath === sourcePath
|
||||
: !part.sourcePath && (clipboard || part.filename === file.name)),
|
||||
)
|
||||
if (duplicate) {
|
||||
input.duplicate()
|
||||
return true
|
||||
}
|
||||
const attachment: PromptInputV2Attachment = {
|
||||
type: "image",
|
||||
id: globalThis.crypto?.randomUUID?.() ?? Math.random().toString(16).slice(2),
|
||||
filename: file.name,
|
||||
sourcePath: input.getPathForFile?.(file) || undefined,
|
||||
sourcePath,
|
||||
mime,
|
||||
dataUrl: url,
|
||||
}
|
||||
@@ -141,7 +159,7 @@ export function createPromptInputV2Attachments(
|
||||
const plainText = clipboardData.getData("text/plain") ?? ""
|
||||
if (input.readClipboardImage && !plainText) {
|
||||
const file = await input.readClipboardImage()
|
||||
if (file && (await add(file, true, target))) return
|
||||
if (file && (await add(file, true, target, true))) return
|
||||
}
|
||||
if (!plainText) return
|
||||
const text = plainText.includes("\r") ? plainText.replace(/\r\n?/g, "\n") : plainText
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
PromptInputV2Suggestion,
|
||||
} from "./types"
|
||||
import type { PromptInputV2Interaction, PromptInputV2SelectControl } from "./interaction"
|
||||
import "./attachments.css"
|
||||
|
||||
export type {
|
||||
PromptInputV2Attachment,
|
||||
@@ -107,7 +108,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
<form
|
||||
data-component="prompt-input-v2"
|
||||
data-dock-border-underlay={props.borderUnderlay ? "v2" : undefined}
|
||||
class="group/prompt-input relative min-h-[96px] w-full rounded-xl bg-v2-background-bg-base"
|
||||
class="group/prompt-input relative min-h-[96px] w-full overflow-clip rounded-xl bg-v2-background-bg-base"
|
||||
classList={{
|
||||
"shadow-[var(--v2-elevation-raised)]": !props.borderUnderlay,
|
||||
"border border-v2-icon-icon-info border-dashed": state.drag === "active",
|
||||
@@ -378,7 +379,7 @@ export function PromptInputV2Attachments(props: {
|
||||
}) {
|
||||
return (
|
||||
<Show when={props.attachments.length > 0 || (props.comments?.length ?? 0) > 0}>
|
||||
<div data-slot="prompt-attachments" class="relative">
|
||||
<div data-component="prompt-input-v2-attachments" data-slot="prompt-attachments" class="relative">
|
||||
<div
|
||||
data-slot="prompt-attachments-scroll"
|
||||
class="flex flex-nowrap gap-2 overflow-x-auto no-scrollbar px-2 pt-2 pb-1"
|
||||
@@ -444,8 +445,14 @@ export function PromptInputV2Attachments(props: {
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div 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 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)]" />
|
||||
<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)]"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 16px 8px 8px;
|
||||
padding: 12px 16px 12px 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
}
|
||||
|
||||
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-filter"] {
|
||||
padding: 0 8px 8px;
|
||||
padding: 0 8px 4px;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
@@ -109,10 +109,71 @@
|
||||
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
timeline-scope: --session-review-v2-sidebar-tree-scroll;
|
||||
}
|
||||
|
||||
@keyframes session-review-v2-sidebar-tree-fade-top {
|
||||
from {
|
||||
visibility: hidden;
|
||||
}
|
||||
to {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes session-review-v2-sidebar-tree-fade-bottom {
|
||||
from {
|
||||
visibility: visible;
|
||||
}
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::before,
|
||||
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
z-index: 10;
|
||||
height: 16px;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::before {
|
||||
top: 0;
|
||||
background: linear-gradient(to bottom, var(--v2-background-bg-base), transparent);
|
||||
}
|
||||
|
||||
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::after {
|
||||
bottom: 0;
|
||||
background: linear-gradient(to top, var(--v2-background-bg-base), transparent);
|
||||
}
|
||||
|
||||
@supports (animation-timeline: --session-review-v2-sidebar-tree-scroll) and
|
||||
(timeline-scope: --session-review-v2-sidebar-tree-scroll) {
|
||||
[data-component="session-review-v2-sidebar-root"]
|
||||
[data-slot="session-review-v2-sidebar-tree"]
|
||||
.scroll-view__viewport {
|
||||
scroll-timeline: --session-review-v2-sidebar-tree-scroll y;
|
||||
}
|
||||
|
||||
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::before {
|
||||
animation: session-review-v2-sidebar-tree-fade-top linear both;
|
||||
animation-timeline: --session-review-v2-sidebar-tree-scroll;
|
||||
animation-range: 0 0.1px;
|
||||
}
|
||||
|
||||
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::after {
|
||||
animation: session-review-v2-sidebar-tree-fade-bottom linear both;
|
||||
animation-timeline: --session-review-v2-sidebar-tree-scroll;
|
||||
animation-range: calc(100% - 1.1px) calc(100% - 1px);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport {
|
||||
padding: 0 8px 12px;
|
||||
padding: 4px 8px 12px;
|
||||
}
|
||||
|
||||
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] .scroll-view__thumb {
|
||||
@@ -244,7 +305,7 @@
|
||||
}
|
||||
|
||||
[data-component="icon-button-v2"][data-variant="ghost"].session-review-v2-sidebar-toggle[aria-expanded="true"] {
|
||||
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||
background-color: var(--v2-background-bg-layer-02);
|
||||
}
|
||||
|
||||
[data-component="session-review-v2"] [data-slot="session-review-v2-file-header"] {
|
||||
|
||||
@@ -165,11 +165,13 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
}
|
||||
|
||||
const prev = () => {
|
||||
return props.files[fileIndex() - 1]
|
||||
if (!canCycle()) return
|
||||
return props.files[(fileIndex() - 1 + props.files.length) % props.files.length]
|
||||
}
|
||||
|
||||
const next = () => {
|
||||
return props.files[fileIndex() + 1]
|
||||
if (!canCycle()) return
|
||||
return props.files[(fileIndex() + 1) % props.files.length]
|
||||
}
|
||||
|
||||
const canCycle = () => props.files.length > 0
|
||||
@@ -183,15 +185,15 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
props.onSelectFile(file)
|
||||
}
|
||||
|
||||
// The prev/next tooltips advertise < and >; keep the keys working while the
|
||||
// Keep the advertised arrow keys working while the
|
||||
// pane is mounted, but never while typing in an input or comment editor.
|
||||
makeEventListener(document, "keydown", (event) => {
|
||||
if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) return
|
||||
if (event.key !== "<" && event.key !== ">") return
|
||||
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return
|
||||
const target = event.target
|
||||
if (target instanceof HTMLElement && (target.isContentEditable || target.closest("input, textarea, select"))) return
|
||||
if (!props.hasDiffs || !canCycle()) return
|
||||
const file = event.key === "<" ? prev() : next()
|
||||
const file = event.key === "ArrowLeft" ? prev() : next()
|
||||
if (!file) return
|
||||
event.preventDefault()
|
||||
cycle(file)
|
||||
@@ -219,7 +221,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
value={
|
||||
<>
|
||||
{i18n.t("ui.sessionReviewV2.previousFile")}
|
||||
<KeybindV2 keys={["<"]} variant="neutral" />
|
||||
<KeybindV2 keys={["←"]} variant="neutral" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -239,7 +241,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
value={
|
||||
<>
|
||||
{i18n.t("ui.sessionReviewV2.nextFile")}
|
||||
<KeybindV2 keys={[">"]} variant="neutral" />
|
||||
<KeybindV2 keys={["→"]} variant="neutral" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -331,6 +333,7 @@ export function SessionReviewV2SidebarToggle(props: { opened: boolean; disabled?
|
||||
class="session-review-v2-sidebar-toggle"
|
||||
aria-label={i18n.t("ui.sessionReviewV2.toggleSidebar")}
|
||||
aria-expanded={props.opened}
|
||||
data-expanded={props.opened ? "" : undefined}
|
||||
disabled={props.disabled}
|
||||
onClick={props.onToggle}
|
||||
icon={<Icon name="filetree" />}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { query } from "@solidjs/router"
|
||||
|
||||
export const modelCatalogSourceUrl = "https://models.dev/catalog.json"
|
||||
export const modelCatalogPricingUrl = "https://models.dev/api.json"
|
||||
export const modelCatalogLabSourceUrl = "https://models.dev/labs"
|
||||
export const modelCatalogSourceUrl = "https://models.opencode.ai/catalog.json"
|
||||
export const modelCatalogPricingUrl = "https://models.opencode.ai/api.json"
|
||||
export const modelCatalogLabSourceUrl = "https://models.opencode.ai/labs"
|
||||
|
||||
export type ModelCatalogCost = {
|
||||
input: number
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -104,6 +104,7 @@
|
||||
"remend": "catalog:",
|
||||
"shiki": "catalog:",
|
||||
"solid-list": "catalog:",
|
||||
"solid-sonner": "catalog:",
|
||||
"strip-ansi": "7.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,58 +1,182 @@
|
||||
[data-component="toast-v2-region"] {
|
||||
position: fixed;
|
||||
bottom: 48px;
|
||||
right: 32px;
|
||||
.toast-v2-region {
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-width: min(320px, calc(100vw - 64px));
|
||||
max-height: none;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
|
||||
[data-slot="toast-v2-list"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
overflow-x: visible;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
[data-component="toast-v2"] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
.toast-v2 {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 20px;
|
||||
column-gap: 12px;
|
||||
row-gap: 12px;
|
||||
width: 320px;
|
||||
max-width: 100%;
|
||||
padding: 12px;
|
||||
max-height: min(420px, calc(100dvh - 96px));
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
transition: transform 140ms ease-out;
|
||||
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
border-radius: 8px;
|
||||
color: var(--v2-text-text-base);
|
||||
background: var(--v2-background-bg-layer-01);
|
||||
box-shadow: var(--v2-elevation-floating);
|
||||
transition:
|
||||
transform 280ms cubic-bezier(0.2, 0, 0, 1),
|
||||
opacity 160ms ease-out,
|
||||
height 280ms cubic-bezier(0.2, 0, 0, 1),
|
||||
box-shadow 160ms ease-out;
|
||||
|
||||
&[data-opened] {
|
||||
animation: toastV2PopIn 140ms ease-out;
|
||||
&:has(> [data-icon]) {
|
||||
grid-template-columns: 16px minmax(0, 1fr) 20px;
|
||||
}
|
||||
|
||||
&[data-closed] {
|
||||
animation: toastV2PopOut 100ms ease-in forwards;
|
||||
&:focus-visible {
|
||||
box-shadow:
|
||||
var(--v2-elevation-floating),
|
||||
0 0 0 2px var(--v2-border-border-focus);
|
||||
}
|
||||
|
||||
&[data-expanded="true"] {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&[data-expanded="false"][data-front="false"] > * {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&[data-expanded="false"][data-front="false"] {
|
||||
--y: translateY(calc(-1 * var(--toasts-before) * 9.5px)) scale(calc(1 - var(--toasts-before) * 0.05));
|
||||
}
|
||||
|
||||
> [data-icon] {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
align-self: start;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 20px;
|
||||
color: var(--v2-icon-icon-base);
|
||||
}
|
||||
|
||||
> [data-content] {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&:has(> [data-icon]) > [data-content] {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
> [data-close-button] {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
position: static;
|
||||
align-self: start;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
transform: none;
|
||||
background: transparent;
|
||||
color: var(--v2-icon-icon-muted);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:has(> [data-icon]) > [data-close-button] {
|
||||
grid-column: 3;
|
||||
}
|
||||
|
||||
> [data-close-button]:hover {
|
||||
background: var(--v2-overlay-simple-overlay-hover);
|
||||
color: var(--v2-icon-icon-base);
|
||||
}
|
||||
|
||||
> [data-close-button]:active {
|
||||
background: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
> [data-close-button]:focus-visible {
|
||||
outline: 2px solid var(--v2-border-border-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
> [data-icon] > *,
|
||||
> [data-icon] svg,
|
||||
> [data-close-button] svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-title],
|
||||
[data-slot="toast-v2-title"] {
|
||||
color: var(--v2-text-text-base);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 530;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.04px;
|
||||
font-synthesis: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-description],
|
||||
[data-slot="toast-v2-description"] {
|
||||
color: var(--v2-text-text-muted);
|
||||
text-wrap-style: pretty;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 440;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.04px;
|
||||
font-synthesis: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-slot="toast-v2-actions"] {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&:has(> [data-icon]) [data-slot="toast-v2-actions"] {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
[data-slot="toast-v2-actions"] [data-component="button-v2"] {
|
||||
min-height: 24px;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 530;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.04px;
|
||||
font-synthesis: none;
|
||||
}
|
||||
|
||||
[data-slot="toast-v2-header"] {
|
||||
@@ -69,34 +193,7 @@
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 20px;
|
||||
min-width: 16px;
|
||||
min-height: 20px;
|
||||
color: var(--v2-icon-icon-base);
|
||||
|
||||
[data-component="icon"] {
|
||||
color: var(--v2-icon-icon-base);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
> * {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="toast-v2-content"] {
|
||||
@@ -104,112 +201,25 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="toast-v2-title"] {
|
||||
color: var(--v2-text-text-base);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 530;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.04px;
|
||||
font-synthesis: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-slot="toast-v2-description"] {
|
||||
color: var(--v2-text-text-muted);
|
||||
text-wrap-style: pretty;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 440;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.04px;
|
||||
font-synthesis: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-slot="toast-v2-actions"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
[data-slot="toast-v2-actions"] [data-component="button-v2"] {
|
||||
min-height: 24px;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 530;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.04px;
|
||||
font-synthesis: none;
|
||||
}
|
||||
|
||||
[data-slot="toast-v2-close-button"] {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--v2-icon-icon-muted);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: var(--v2-overlay-simple-overlay-hover);
|
||||
color: var(--v2-icon-icon-base);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--v2-border-border-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.toast-v2,
|
||||
.toast-v2 > * {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastV2PopIn {
|
||||
from {
|
||||
opacity: 0.8;
|
||||
transform: translateY(6px);
|
||||
@media (max-width: 600px) {
|
||||
.toast-v2-region {
|
||||
max-width: calc(100vw - 32px);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastV2PopOut {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateY(6px);
|
||||
opacity: 0.8;
|
||||
.toast-v2 {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ Use brief titles/descriptions; limit actions to 1-2.
|
||||
- Toasts render in a portal and auto-dismiss unless persistent.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm aria-live behavior from Kobalte Toast.
|
||||
- The region announces additions through an ARIA live region and expands with Alt+T.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="toast-v2"\` and slot data attributes.
|
||||
@@ -57,10 +57,10 @@ export const AllExamples = {
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M13.5554 10.4453V13.5564H11.7777H4.22211C3.23989 13.5564 2.44434 13.5564 2.44434 13.5564V10.4453"
|
||||
stroke="var(--icon-icon-base)"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<path d="M4.88867 6L7.99978 9.11111L11.1109 6" stroke="var(--icon-icon-base)" />
|
||||
<path d="M8 9.11198V2.44531" stroke="var(--icon-icon-base)" />
|
||||
<path d="M4.88867 6L7.99978 9.11111L11.1109 6" stroke="currentColor" />
|
||||
<path d="M8 9.11198V2.44531" stroke="currentColor" />
|
||||
</svg>
|
||||
),
|
||||
actions: [
|
||||
|
||||
@@ -1,40 +1,78 @@
|
||||
import { Toast as Kobalte, toaster } from "@kobalte/core/toast"
|
||||
import type { ToastRootProps, ToastCloseButtonProps, ToastTitleProps, ToastDescriptionProps } from "@kobalte/core/toast"
|
||||
import { Toaster, toast, type ToasterProps } from "solid-sonner"
|
||||
import type { ComponentProps, JSX } from "solid-js"
|
||||
import { Show, children } from "solid-js"
|
||||
import { createContext, onCleanup, onMount, splitProps, useContext } from "solid-js"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { ButtonV2 } from "./button-v2"
|
||||
import "./button-v2.css"
|
||||
import "./toast-v2.css"
|
||||
|
||||
export interface ToastV2RegionProps extends ComponentProps<typeof Kobalte.Region> {}
|
||||
export interface ToastV2RegionProps extends ToasterProps {}
|
||||
|
||||
function ToastV2Region(props: ToastV2RegionProps) {
|
||||
const [local, rest] = splitProps(props, ["class", "className", "style", "toastOptions", "swipeDirections"])
|
||||
onMount(() => {
|
||||
const sync = () => {
|
||||
document.querySelectorAll<HTMLElement>(".toast-v2-region .toast-v2").forEach((element) => {
|
||||
const hidden = element.dataset.visible === "false"
|
||||
element.inert = hidden
|
||||
element.tabIndex = hidden ? -1 : 0
|
||||
})
|
||||
}
|
||||
let connected = false
|
||||
const connect = () => {
|
||||
const regions = document.querySelectorAll(".toast-v2-region")
|
||||
if (!regions.length) return
|
||||
observer.disconnect()
|
||||
regions.forEach((region) => {
|
||||
observer.observe(region, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["data-visible"],
|
||||
})
|
||||
})
|
||||
connected = true
|
||||
sync()
|
||||
}
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!connected) connect()
|
||||
else sync()
|
||||
})
|
||||
observer.observe(document.body, { subtree: true, childList: true })
|
||||
queueMicrotask(connect)
|
||||
onCleanup(() => observer.disconnect())
|
||||
})
|
||||
return (
|
||||
<Portal>
|
||||
<Kobalte.Region data-component="toast-v2-region" {...props}>
|
||||
<Kobalte.List data-slot="toast-v2-list" />
|
||||
</Kobalte.Region>
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
offset={{ right: 32, bottom: 48 }}
|
||||
mobileOffset={16}
|
||||
gap={12}
|
||||
duration={5000}
|
||||
swipeDirections={["bottom"]}
|
||||
className={["toast-v2-region", local.className, local.class].filter(Boolean).join(" ")}
|
||||
style={{ "--width": "320px", ...local.style } as JSX.CSSProperties}
|
||||
toastOptions={{
|
||||
...local.toastOptions,
|
||||
unstyled: true,
|
||||
closeButton: true,
|
||||
closeButtonAriaLabel: local.toastOptions?.closeButtonAriaLabel ?? "Dismiss",
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ToastV2RootComponentProps extends ToastRootProps {
|
||||
class?: string
|
||||
classList?: ComponentProps<"li">["classList"]
|
||||
const ToastV2Context = createContext<number>()
|
||||
|
||||
export interface ToastV2RootComponentProps {
|
||||
toastId: number
|
||||
children?: JSX.Element
|
||||
}
|
||||
|
||||
function ToastV2Root(props: ToastV2RootComponentProps) {
|
||||
return (
|
||||
<Kobalte
|
||||
data-component="toast-v2"
|
||||
classList={{
|
||||
...props.classList,
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return <ToastV2Context.Provider value={props.toastId}>{props.children}</ToastV2Context.Provider>
|
||||
}
|
||||
|
||||
function ToastV2Icon(props: ComponentProps<"div">) {
|
||||
@@ -45,26 +83,43 @@ function ToastV2Content(props: ComponentProps<"div">) {
|
||||
return <div data-slot="toast-v2-content" {...props} />
|
||||
}
|
||||
|
||||
function ToastV2Title(props: ToastTitleProps & ComponentProps<"div">) {
|
||||
return <Kobalte.Title data-slot="toast-v2-title" {...props} />
|
||||
function ToastV2Title(props: ComponentProps<"div">) {
|
||||
return <div data-slot="toast-v2-title" {...props} />
|
||||
}
|
||||
|
||||
function ToastV2Description(props: ToastDescriptionProps & ComponentProps<"div">) {
|
||||
return <Kobalte.Description data-slot="toast-v2-description" {...props} />
|
||||
function ToastV2Description(props: ComponentProps<"div">) {
|
||||
return <div data-slot="toast-v2-description" {...props} />
|
||||
}
|
||||
|
||||
function ToastV2Actions(props: ComponentProps<"div">) {
|
||||
return <div data-slot="toast-v2-actions" {...props} />
|
||||
}
|
||||
|
||||
function ToastV2CloseButton(props: ToastCloseButtonProps & ComponentProps<"button">) {
|
||||
function ToastV2CloseButton(props: ComponentProps<"button">) {
|
||||
const toastId = useContext(ToastV2Context)
|
||||
const [local, rest] = splitProps(props, ["children", "onClick"])
|
||||
return (
|
||||
<Kobalte.CloseButton data-slot="toast-v2-close-button" aria-label="Dismiss" {...props}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M4.25 11.75L11.75 4.25" stroke="currentColor" />
|
||||
<path d="M11.75 11.75L4.25 4.25" stroke="currentColor" />
|
||||
</svg>
|
||||
</Kobalte.CloseButton>
|
||||
<button
|
||||
type="button"
|
||||
data-slot="toast-v2-close-button"
|
||||
aria-label="Dismiss"
|
||||
{...rest}
|
||||
onClick={(event) => {
|
||||
if (typeof local.onClick === "function") local.onClick(event)
|
||||
if (!event.defaultPrevented && toastId !== undefined) toasterV2.dismiss(toastId)
|
||||
}}
|
||||
>
|
||||
{local.children ?? <CloseIcon />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function CloseIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M4.25 11.75L11.75 4.25" stroke="currentColor" />
|
||||
<path d="M11.75 11.75L4.25 4.25" stroke="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,7 +133,29 @@ export const ToastV2 = Object.assign(ToastV2Root, {
|
||||
CloseButton: ToastV2CloseButton,
|
||||
})
|
||||
|
||||
export { toaster as toasterV2 }
|
||||
let toastV2Id = 0
|
||||
|
||||
export const toasterV2 = {
|
||||
show(render: (props: { toastId: number }) => JSX.Element, options?: { duration?: number; persistent?: boolean }) {
|
||||
const toastId = --toastV2Id
|
||||
toast.custom((id) => render({ toastId: Number(id) }), {
|
||||
id: toastId,
|
||||
className: "toast-v2",
|
||||
duration: options?.persistent ? Number.POSITIVE_INFINITY : options?.duration,
|
||||
unstyled: true,
|
||||
})
|
||||
return toastId
|
||||
},
|
||||
dismiss(toastId?: number) {
|
||||
if (toastId === undefined) {
|
||||
activeToastV2ByKey.clear()
|
||||
activeToastV2ById.clear()
|
||||
} else {
|
||||
releaseToastV2(activeToastV2ById.get(toastId))
|
||||
}
|
||||
return toast.dismiss(toastId)
|
||||
},
|
||||
}
|
||||
|
||||
export interface ToastV2Action {
|
||||
label: string
|
||||
@@ -90,53 +167,107 @@ export interface ToastV2Options {
|
||||
title?: string
|
||||
description?: string
|
||||
icon?: JSX.Element
|
||||
variant?: "default" | "success" | "error" | "loading"
|
||||
duration?: number
|
||||
persistent?: boolean
|
||||
actions?: ToastV2Action[]
|
||||
}
|
||||
|
||||
interface ActiveToastV2 {
|
||||
id: number
|
||||
key: string
|
||||
options: ToastV2Options
|
||||
actions?: JSX.Element
|
||||
}
|
||||
|
||||
const activeToastV2ByKey = new Map<string, ActiveToastV2>()
|
||||
const activeToastV2ById = new Map<number, ActiveToastV2>()
|
||||
|
||||
export function showToastV2(options: ToastV2Options | string) {
|
||||
const opts = typeof options === "string" ? { description: options } : options
|
||||
return toaster.show((props) => {
|
||||
const resolvedIcon = children(() => opts.icon)
|
||||
return (
|
||||
<ToastV2 toastId={props.toastId} duration={opts.duration} persistent={opts.persistent}>
|
||||
<div data-slot="toast-v2-header">
|
||||
<Show when={resolvedIcon()}>
|
||||
<ToastV2.Icon>{resolvedIcon()}</ToastV2.Icon>
|
||||
</Show>
|
||||
<ToastV2.Content>
|
||||
<Show when={opts.title}>
|
||||
<ToastV2.Title>{opts.title}</ToastV2.Title>
|
||||
</Show>
|
||||
<Show when={opts.description}>
|
||||
<ToastV2.Description>{opts.description}</ToastV2.Description>
|
||||
</Show>
|
||||
</ToastV2.Content>
|
||||
<ToastV2.CloseButton />
|
||||
</div>
|
||||
<Show when={opts.actions?.length}>
|
||||
<ToastV2.Actions>
|
||||
{opts.actions!.map((action) => (
|
||||
<ButtonV2
|
||||
variant={action.variant === "secondary" ? "ghost" : "neutral"}
|
||||
size="small"
|
||||
data-action-variant={action.variant ?? "primary"}
|
||||
onClick={() => {
|
||||
if (typeof action.onClick === "function") {
|
||||
action.onClick()
|
||||
}
|
||||
toaster.dismiss(props.toastId)
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
</ButtonV2>
|
||||
))}
|
||||
</ToastV2.Actions>
|
||||
</Show>
|
||||
</ToastV2>
|
||||
)
|
||||
const opts: ToastV2Options = typeof options === "string" ? { description: options } : options
|
||||
const key = JSON.stringify({
|
||||
title: opts.title,
|
||||
description: opts.description,
|
||||
variant: opts.variant,
|
||||
duration: opts.duration,
|
||||
persistent: opts.persistent,
|
||||
actions: opts.actions?.map((action) => [action.label, action.variant]),
|
||||
})
|
||||
const active = activeToastV2ByKey.get(key)
|
||||
const toasts = toast.getToasts()
|
||||
|
||||
if (active && toasts.at(-1)?.id === active.id) {
|
||||
active.options = opts
|
||||
publishToastV2(active)
|
||||
pulseToastV2(active.id)
|
||||
return active.id
|
||||
}
|
||||
|
||||
if (active && toasts.some((item) => item.id === active.id)) toasterV2.dismiss(active.id)
|
||||
releaseToastV2(active)
|
||||
|
||||
const entry: ActiveToastV2 = { id: --toastV2Id, key, options: opts }
|
||||
entry.actions = createToastV2Actions(entry)
|
||||
activeToastV2ByKey.set(key, entry)
|
||||
activeToastV2ById.set(entry.id, entry)
|
||||
publishToastV2(entry)
|
||||
return entry.id
|
||||
}
|
||||
|
||||
function publishToastV2(entry: ActiveToastV2) {
|
||||
const release = () => releaseToastV2(entry)
|
||||
toast(entry.options.title ?? "", {
|
||||
id: entry.id,
|
||||
description: entry.options.description,
|
||||
icon: entry.options.icon,
|
||||
action: entry.actions,
|
||||
closeButton: true,
|
||||
duration: entry.options.persistent ? Number.POSITIVE_INFINITY : entry.options.duration,
|
||||
className: `toast-v2 toast-v2--${entry.options.variant ?? "default"}`,
|
||||
testId: `toast-v2-${entry.id}`,
|
||||
unstyled: true,
|
||||
onDismiss: release,
|
||||
onAutoClose: release,
|
||||
})
|
||||
}
|
||||
|
||||
function createToastV2Actions(entry: ActiveToastV2) {
|
||||
if (!entry.options.actions?.length) return undefined
|
||||
return (
|
||||
<ToastV2.Actions>
|
||||
{entry.options.actions.map((action, index) => (
|
||||
<button
|
||||
type="button"
|
||||
data-component="button-v2"
|
||||
data-variant={action.variant === "secondary" ? "ghost" : "neutral"}
|
||||
data-size="small"
|
||||
data-action-variant={action.variant ?? "primary"}
|
||||
onClick={() => {
|
||||
const onClick = entry.options.actions?.[index]?.onClick
|
||||
if (typeof onClick === "function") onClick()
|
||||
toasterV2.dismiss(entry.id)
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
</ToastV2.Actions>
|
||||
)
|
||||
}
|
||||
|
||||
function pulseToastV2(toastId: number) {
|
||||
if (typeof document === "undefined" || typeof requestAnimationFrame === "undefined") return
|
||||
requestAnimationFrame(() => {
|
||||
const element = document.querySelector<HTMLElement>(`[data-testid="toast-v2-${toastId}"]`)
|
||||
if (!element || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
|
||||
element.animate([{ scale: 1 }, { scale: 1.025 }, { scale: 1 }], { duration: 160, easing: "ease-out" })
|
||||
})
|
||||
}
|
||||
|
||||
function releaseToastV2(entry: ActiveToastV2 | undefined) {
|
||||
if (!entry) return
|
||||
if (activeToastV2ByKey.get(entry.key) === entry) activeToastV2ByKey.delete(entry.key)
|
||||
if (activeToastV2ById.get(entry.id) === entry) activeToastV2ById.delete(entry.id)
|
||||
}
|
||||
|
||||
export interface ToastV2PromiseOptions<T, U = unknown> {
|
||||
|
||||
@@ -45,7 +45,7 @@ function providerIconsPlugin() {
|
||||
}
|
||||
|
||||
async function fetchProviderIcons() {
|
||||
const url = process.env.OPENCODE_MODELS_URL || "https://models.dev"
|
||||
const url = process.env.OPENCODE_MODELS_URL || "https://models.opencode.ai"
|
||||
const providers = await fetch(`${url}/api.json`)
|
||||
.then((res) => res.json())
|
||||
.then((json) => Object.keys(json))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -54,6 +54,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
|
||||
| [opencode-firecrawl](https://github.com/firecrawl/opencode-firecrawl) | Web scraping, crawling, and search via the Firecrawl CLI |
|
||||
| [opencode-jfrog-plugin](https://github.com/jfrog/opencode-jfrog-plugin) | JFrog Plugin for seamless integration of Opencode users to JFrog platform |
|
||||
| [opencode-goal-plugin](https://github.com/willytop8/OpenCode-goal-plugin) | Session-scoped `/goal` workflow that keeps objectives in context and auto-continues until complete |
|
||||
| [opencode-tavily](https://github.com/tavily-ai/opencode-tavily) | Web search, extraction, crawling, and deep research via the Tavily CLI |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.10",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user