diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 2a3b540586..e2dcdb39e1 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -45,6 +45,7 @@ upstream maintainers — see README → Syncing with upstream for why and how. | `packages/tui/src/component/dialog-provider.tsx`, `packages/tui/src/component/dialog-model.tsx`, `packages/opencode/src/cli/cmd/providers.ts` | Filter `opencode`/`opencode-go` out of provider pickers and the `/model` dialog (model list + Favorites/Recent; the model dialog's opencode-first sort became a plain name sort) | Upstream's hosted offerings (Zen/Go) are detours for gateway users. | Keep our filters. | | `packages/tui/src/component/prompt/index.tsx` | Gated the prompt footer's model/provider indicator on `useConnected()` (the shared "a real provider is configured" predicate) | Don't advertise the free hosted Zen default (e.g. "Big Pickle · OpenCode Zen") in the footer while nothing is configured — same rationale and predicate as `app.tsx`'s startup gate and the provider/model filters. A configured gateway/real provider, or a deliberately-authenticated opencode/opencode-go, still shows its model. | Keep the extra `Show` condition; take upstream's surrounding changes. | | `packages/app/index.html` | `` → "CoDev Code" | Web UI tab title. | Keep our title. | +| `packages/app/src/pages/layout.tsx`, `packages/app/src/pages/layout-new.tsx` | Dropped the `<TabsInfoPopup />` mount and its import from both layouts (added upstream in v1.17.18); `HelpButton` stays. `help-button.tsx` itself is left upstream — `TabsInfoPopup` remains defined/exported there, just unmounted (tree-shaken out of the build). | `TabsInfoPopup` is a dev-channel-only "Introducing Tabs" promo whose `muted autoplay loop` `<video>` mounts on every layout render. The fork's Windows e2e runs on GPU-less GitHub-hosted runners (software GPU — see the runner-swap row), where continuous video decode wedges the GPU command buffer (`kTransientFailure`), making the whole app-e2e suite flaky and ~3–5× slower. It never ships (release builds use a non-`dev` `CODEV_CHANNEL`), so unmounting it costs users nothing. | Keep ours: don't re-add the `<TabsInfoPopup />` mount or restore it to the import. Leave upstream's `help-button.tsx` (incl. the `TabsInfoPopup` definition) untouched so it never conflicts. | | `packages/app/src/desktop-menu.ts`, `packages/app/src/components/windows-app-menu.tsx` | App-menu label "OpenCode" → "CoDev Code" | Visible menu branding. | Keep our labels. | | `packages/desktop/src/main/index.ts`, `packages/desktop/src/main/windows.ts` | APP_NAMES + window title → "CoDev Code" (Dev/Beta variants too) | Desktop app display name. | Keep our names. | | `packages/desktop/electron-builder.config.ts` | `productName` ×3 → "CoDev Code…"; artifactName → `codev-code-desktop-…`; protocol display name | Installer/product branding. appId (`ai.opencode.desktop`) and the `opencode://` deep-link scheme stay upstream — identity plumbing wired into main/index.ts and app deep-links; do not rename casually. | Keep our names; take upstream's other config. | @@ -64,7 +65,7 @@ upstream maintainers — see README → Syncing with upstream for why and how. | `.github/workflows/` — 22 upstream workflows deleted (beta, close-issues, close-prs, compliance-close, containers, deploy, docs-locale-sync, docs-update, duplicate-issues, generate, nix-hashes, notify-discord, opencode, pr-management, pr-standards, publish, publish-github-action, publish-vscode, release-github-action, review, stats, triage) | Deleted | All inert or upstream-only here: gated on `anomalyco/opencode`, publish/deploy to upstream infra, need upstream secrets (`OPENCODE_APP_SECRET`, `OPENCODE_API_KEY`, marketplace/Discord/PostHog tokens), trigger only on a `dev` branch the fork lacks, or read the deleted `TEAM_MEMBERS`. | Keep deleted; delete new upstream workflows too unless they're generic self-contained CI. | | `.github/workflows/{typecheck,test,nix-eval,storybook}.yml` | Triggers `dev` → `main` (incl. test.yml's concurrency-group ref check); runners `blacksmith-*` → `ubuntu-latest`/`windows-latest`; test.yml unit-test step `timeout-minutes` 20 → 45; test.yml exerciser step gains `timeout-minutes: 30`; test.yml Windows unit job pre-installs ripgrep via choco | Fork's default branch is `main`; Blacksmith runners aren't installed here, so jobs queued forever. GitHub-hosted `windows-latest` needs well over 20 minutes for the spawn-heavy opencode suite. A wedged exerciser run otherwise holds the job for GitHub's 6-hour ceiling. The file-search index is built by a background ripgrep walk with swallowed errors; Windows's first-use rg.exe download left it permanently empty, failing httpapi-file's search test every run. | Keep our branches + runners + timeouts; take upstream's other changes. | | `packages/core/test/ripgrep.test.ts` | `120_000` timeout on both `it.live` tests | First use downloads + extracts the ripgrep binary; GitHub-hosted Windows runners blow bun's 5s default. | Keep our timeouts; take upstream's test changes. | -| `packages/opencode/test/cli/run/run-process.test.ts`, `packages/opencode/test/cli/acp/lifecycle.test.ts`, `packages/opencode/test/server/httpapi-file.test.ts`, `packages/opencode/test/server/httpapi-sdk.test.ts`, `packages/opencode/test/effect/runner.test.ts`, `packages/opencode/test/session/prompt.test.ts`, `packages/opencode/test/project/instance-bootstrap.test.ts` | Slow-runner headroom: unknown-model test `timeoutMs`/bound 15s → 30s (test timeout 60s); ACP stdin-EOF exit timeout 5s → 30s; file-search-index poll 5s → 120s with a 150s test timeout (the search-endpoints test pays ripgrep's first-use download on Windows runners, like ripgrep.test.ts); Runner fiber-coordination `Effect.timeout` guards 250ms/1s → 10s (pure hang-detectors on already-unblocked awaits; Windows runners starve the event loop past 250ms); prompt.test.ts's two `sleep`-based loop/shell tests ("loop waits while shell runs…", "shell completion resumes queued loop callers") moved from `it.instance` to the file's own `unix` helper so they skip on Windows like their `unixNoLLMServer` siblings (`sleep` isn't on Windows, so the shell fiber hangs to the budget — a genuine hang, not slowness); instance-bootstrap's first test 30s → 60s (pays instance cold-start) | Loaded GitHub-hosted runners spawn processes, fetch ripgrep, and build the lazy file index slower than upstream's Blacksmith runners; 250ms–30s budgets flake or fail outright, and Unix-shell tests can't run on Windows at all. Hang regressions are still caught (a hang exceeds the bound; polls return as soon as ready). | Keep our numbers and the `unix` gating; take upstream's test changes. | +| `packages/opencode/test/cli/run/run-process.test.ts`, `packages/opencode/test/cli/acp/lifecycle.test.ts`, `packages/opencode/test/server/httpapi-file.test.ts`, `packages/opencode/test/server/httpapi-sdk.test.ts`, `packages/opencode/test/effect/runner.test.ts`, `packages/opencode/test/session/prompt.test.ts`, `packages/opencode/test/project/instance-bootstrap.test.ts` | Slow-runner headroom: unknown-model test `timeoutMs`/bound 15s → 30s (test timeout 60s); the two unknown-stream-finish `codev run` subprocess tests ("unknown stream finish preserves partial output…", "--format json records partial output for an unknown stream finish") `timeoutMs` 30s → 60s and test timeout 60s → 120s (pure-upstream spawn tests that pay `codev run` cold-start; neither asserts on duration, so headroom only); ACP stdin-EOF exit timeout 5s → 30s; file-search-index poll 5s → 120s with a 150s test timeout (the search-endpoints test pays ripgrep's first-use download on Windows runners, like ripgrep.test.ts); Runner fiber-coordination `Effect.timeout` guards 250ms/1s → 10s (pure hang-detectors on already-unblocked awaits; Windows runners starve the event loop past 250ms); prompt.test.ts's two `sleep`-based loop/shell tests ("loop waits while shell runs…", "shell completion resumes queued loop callers") moved from `it.instance` to the file's own `unix` helper so they skip on Windows like their `unixNoLLMServer` siblings (`sleep` isn't on Windows, so the shell fiber hangs to the budget — a genuine hang, not slowness); instance-bootstrap's first test 30s → 60s (pays instance cold-start) | Loaded GitHub-hosted runners spawn processes, fetch ripgrep, and build the lazy file index slower than upstream's Blacksmith runners; 250ms–30s budgets flake or fail outright, and Unix-shell tests can't run on Windows at all. Hang regressions are still caught (a hang exceeds the bound; polls return as soon as ready). | Keep our numbers and the `unix` gating; take upstream's test changes. | | `packages/opencode/test/tool/external-directory.test.ts`, `packages/opencode/test/tool/read.test.ts` | Windows path-variant tests keep the drive letter in the `alt` form (drop the `.replace(/^[A-Za-z]:/, "")` step) | A drive-less path is drive-relative: it resolves against the CWD's drive (D: on GitHub-hosted Windows) while the temp dir lives on C:, so the file genuinely isn't there. Slash + case variants still exercise normalization. | Keep our `alt` construction; take upstream's test changes. | | `packages/opencode/test/server/httpapi-exercise/environment.ts` | Exercise config/data dirs `config/opencode`,`data/opencode` → `…/codev`; seeds `codev.json` with `{"share":"manual"}` | Follows the `global.ts` app-dir rename (row above); the fork's `share: "disabled"` default (config.ts row) would 500 the share/unshare scenarios, so the isolated environment opts back in (network stays stubbed by `CODEV_DISABLE_SHARE`). Seed file follows the config-file rename row (global `config.json` is no longer read). | Keep our dir names + seeded config; take upstream's surrounding changes. | | `sdks/vscode/package.json`, `sdks/vscode/README.md` | `description` "opencode for VS Code" → "CoDev Code for VS Code"; README title → "CoDev Code VS Code Extension" | Manifest branding. `name`, `displayName`, command IDs/titles and README body (which names actual UI elements) stay upstream — the fork doesn't publish this extension; revisit before shipping it. | Keep our description + title; take upstream's other fields/body. | diff --git a/bun.lock b/bun.lock index 03e66885b0..5cf451bbe5 100644 --- a/bun.lock +++ b/bun.lock @@ -29,8 +29,9 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { + "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", "@dnd-kit/dom": "0.5.0", "@dnd-kit/helpers": "0.5.0", @@ -94,7 +95,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.17.16", + "version": "1.17.18", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -142,7 +143,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -163,7 +164,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -199,7 +200,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -226,7 +227,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -248,7 +249,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -272,7 +273,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -292,7 +293,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.17.16", + "version": "1.17.18", "bin": { "opencode": "./bin/opencode", }, @@ -386,7 +387,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -440,7 +441,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -454,7 +455,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "effect": "catalog:", }, @@ -466,7 +467,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -498,7 +499,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -514,7 +515,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -545,7 +546,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -564,7 +565,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.17.16-1", + "version": "1.17.18", "bin": { "codev": "./bin/opencode", }, @@ -695,7 +696,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -771,7 +772,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "cross-spawn": "catalog:", }, @@ -786,7 +787,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -801,7 +802,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -845,7 +846,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -858,7 +859,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@ibm/plex": "6.4.1", "@opencode-ai/stats-core": "workspace:*", @@ -891,7 +892,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -910,7 +911,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -951,7 +952,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@codev/gateway": "workspace:*", "@opencode-ai/core": "workspace:*", @@ -979,7 +980,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1030,7 +1031,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -1095,6 +1096,7 @@ }, "catalog": { "@cloudflare/workers-types": "4.20251008.0", + "@corvu/drawer": "0.2.4", "@effect/opentelemetry": "4.0.0-beta.83", "@effect/platform-node": "4.0.0-beta.83", "@effect/sql-sqlite-bun": "4.0.0-beta.83", @@ -1478,6 +1480,10 @@ "@codev/gateway": ["@codev/gateway@workspace:packages/codev-gateway"], + "@corvu/dialog": ["@corvu/dialog@0.2.4", "", { "dependencies": { "@corvu/utils": "~0.4.2", "solid-dismissible": "~0.1.1", "solid-focus-trap": "~0.1.8", "solid-presence": "~0.2.0", "solid-prevent-scroll": "~0.1.10" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-n54vJq+fOy8GVrnYBdJpD6JXNuyx7LOeMrRxwzAvZnYGpW8+AA12tnb/P/2emJj/HjOO5otheGKb0breshdFlA=="], + + "@corvu/drawer": ["@corvu/drawer@0.2.4", "", { "dependencies": { "@corvu/dialog": "~0.2.4", "@corvu/utils": "~0.4.2", "@solid-primitives/memo": "^1.4.1", "solid-transition-size": "~0.1.4" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-7jQoGZ8ROB9CmXam2nMY2wEskU3IoFwZQywkF/7vrBc/edGsPv7mOVQ1GN6G+4nd7nrMZ3UtqHAhmRh9V0azlw=="], + "@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], @@ -2678,6 +2684,8 @@ "@solid-primitives/media": ["@solid-primitives/media@2.3.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hQ4hLOGvfbugQi5Eu1BFWAIJGIAzztq9x0h02xgBGl2l0Jaa3h7tg6bz5tV1NSuNYVGio4rPoa7zVQQLkkx9dA=="], + "@solid-primitives/memo": ["@solid-primitives/memo@1.5.1", "", { "dependencies": { "@solid-primitives/scheduled": "^1.5.3", "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-VDPrkl9epp0tbby9MvsqphGFCYCtDRC5J8FKzTqHbQiG5hhR8n6xv4MfjhTW231IaBxxPHLxS43EE8c5Q23mSQ=="], + "@solid-primitives/props": ["@solid-primitives/props@3.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA=="], "@solid-primitives/refs": ["@solid-primitives/refs@1.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA=="], @@ -5110,6 +5118,10 @@ "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + "solid-dismissible": ["solid-dismissible@0.1.1", "", { "dependencies": { "@corvu/utils": "~0.4.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-9kcKBJIMdS+586cA1g63HYWxKh3h89leeNHbPZ1csYjuni+NvPBtNr11l0iEX2AKKEt6FHk6qNhc/gjoYAW1pA=="], + + "solid-focus-trap": ["solid-focus-trap@0.1.9", "", { "dependencies": { "@corvu/utils": "~0.4.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-LTyNki6GUJPRLXV5uMWPkYClB07SUMubbr2EkAddiR0CJCF/I283txilMU9RURSr/P8EewMfXWu2o3aWrK7A5A=="], + "solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="], "solid-list": ["solid-list@0.3.0", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg=="], @@ -5122,6 +5134,8 @@ "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=="], + "solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="], "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], @@ -6100,6 +6114,8 @@ "@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], + "@solid-primitives/memo/@solid-primitives/utils": ["@solid-primitives/utils@6.4.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ISSB5QX1qP2ynrheIpYwc4oKR5Ny4siNuUyf1qZniy+Il+p/PtDB0QK1Dnle8noiHpwRD3gpPdubOC3qI/Zamg=="], + "@solidjs/start/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], "@solidjs/start/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], @@ -6460,6 +6476,8 @@ "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + "solid-transition-size/@corvu/utils": ["@corvu/utils@0.3.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.7" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ZWlyWEE8qV9+CB9OAyo2bTrZGXQN9ZeM+JfYv89zoR+lRACKTDuoOZEdiyL8Uc7U5dUSH1uTqKhTTnaHWb+wZA=="], + "sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], "sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], diff --git a/nix/hashes.json b/nix/hashes.json index a7bc1549ab..50208fdbbe 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-DDHxztBr46Jxrb0uYvLYSezrqMgY+UrozRHvKdEyeXY=", - "aarch64-linux": "sha256-Dp2ohpv1jXcFX2ri4fVmsnC9WEx60bviUSlcOtIJBmE=", - "aarch64-darwin": "sha256-CXFJmkcRAS+liHf9TG5qoUG08VvKe5RusEGZTlnNer4=", - "x86_64-darwin": "sha256-7iuCfAzggMDyJ7tX8UuS7T7DCbr9h+lskE+HVFUaD0Q=" + "x86_64-linux": "sha256-JTtn+wXTXg+yklvIMDLcGFaYhTU6ZrCgKT9JTNEQ3gA=", + "aarch64-linux": "sha256-gXU6zyhvAZrZirkL/PlHdkHtEof/7PVSPCaE34Jnd4U=", + "aarch64-darwin": "sha256-Q0oTG3uzOlD/X2kJingLle529lKFoTpyCW2rHXOZ6iE=", + "x86_64-darwin": "sha256-LINvKHxPibTlJeNzfACQx0x+Yj5oROT6Du3I5AtqqXk=" } } diff --git a/package.json b/package.json index 1408e71908..39441aecd9 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@shikijs/stream": "4.2.0", "ulid": "3.0.1", "@kobalte/core": "0.13.11", + "@corvu/drawer": "0.2.4", "@types/luxon": "3.7.1", "@types/node": "24.12.2", "@types/semver": "7.7.1", diff --git a/packages/app/package.json b/packages/app/package.json index cc6f580a5b..510e2015db 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.17.16", + "version": "1.17.18", "description": "", "type": "module", "exports": { @@ -47,6 +47,7 @@ "vite-plugin-solid": "catalog:" }, "dependencies": { + "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", "@dnd-kit/dom": "0.5.0", "@dnd-kit/helpers": "0.5.0", diff --git a/packages/app/src/assets/help/introducing-tabs.mp4 b/packages/app/src/assets/help/introducing-tabs.mp4 new file mode 100644 index 0000000000..2bcc8aeef6 Binary files /dev/null and b/packages/app/src/assets/help/introducing-tabs.mp4 differ diff --git a/packages/app/src/assets/help/placeholder.png b/packages/app/src/assets/help/placeholder.png new file mode 100644 index 0000000000..c651d06cc9 Binary files /dev/null and b/packages/app/src/assets/help/placeholder.png differ diff --git a/packages/app/src/components/dialog-select-model-unpaid-v2.tsx b/packages/app/src/components/dialog-select-model-unpaid-v2.tsx new file mode 100644 index 0000000000..0ce9f18c16 --- /dev/null +++ b/packages/app/src/components/dialog-select-model-unpaid-v2.tsx @@ -0,0 +1,173 @@ +import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2" +import { Icon } from "@opencode-ai/ui/v2/icon" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { ScrollView } from "@opencode-ai/ui/scroll-view" +import { Tag } from "@opencode-ai/ui/v2/badge-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { createMemo, onCleanup, onMount, type Component, For, Show } from "solid-js" +import { useLocal } from "@/context/local" +import { popularProviders, useProviders } from "@/hooks/use-providers" +import { decode64 } from "@/utils/base64" +import { useLanguage } from "@/context/language" +import { ModelTooltip } from "./model-tooltip" + +type ModelState = ReturnType<typeof useLocal>["model"] + +export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (props) => { + const local = useLocal() + const model = props.model ?? local.model + const dialog = useDialog() + const directory = () => decode64(local.slug()) + const providers = useProviders(directory) + const language = useLanguage() + const modelKey = (item: ReturnType<ModelState["list"]>[number]) => `${item.provider.id}:${item.id}` + const currentKey = createMemo(() => { + const c = model.current() + return c ? `${c.provider.id}:${c.id}` : undefined + }) + const isFree = (item: ReturnType<ModelState["list"]>[number]) => + item.provider.id === "opencode" && (!item.cost || item.cost.input === 0) + + const openProviders = (provider?: string) => { + void import("./dialog-connect-provider").then((x) => { + const controller = x.useProviderConnectController() + controller.select(provider) + void dialog.show(() => <x.DialogConnectProvider controller={controller} directory={directory} />) + }) + } + + const selectModel = (item: ReturnType<ModelState["list"]>[number]) => { + model.set({ modelID: item.id, providerID: item.provider.id }, { recent: true }) + dialog.close() + } + + // Focus starts on the dialog's close button, outside the list, so listen at the + // document level while the dialog is mounted instead of on the list container. + let listEl: HTMLDivElement | undefined + onMount(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return + if (!listEl) return + const buttons = Array.from(listEl.querySelectorAll<HTMLButtonElement>("button")) + if (buttons.length === 0) return + const index = buttons.indexOf(document.activeElement as HTMLButtonElement) + const next = + index < 0 ? (e.key === "ArrowDown" ? 0 : buttons.length - 1) : index + (e.key === "ArrowDown" ? 1 : -1) + buttons[(next + buttons.length) % buttons.length]?.focus() + e.preventDefault() + } + document.addEventListener("keydown", handleKeyDown) + onCleanup(() => document.removeEventListener("keydown", handleKeyDown)) + }) + + return ( + <DialogV2 containerClass="!h-[min(calc(100vh_-_16px),480px)] !w-[min(calc(100vw_-_16px),560px)]"> + <DialogHeader closeLabel={language.t("common.close")}> + <DialogTitle>{language.t("dialog.model.select.title")}</DialogTitle> + </DialogHeader> + <div class="h-px w-full shrink-0 bg-v2-border-border-muted" /> + <DialogBody class="min-h-0 flex-1 gap-0"> + <ScrollView class="min-h-0 flex-1 w-full"> + <div ref={listEl} class="flex min-h-full flex-col"> + <div class="flex h-fit w-full flex-col items-start gap-0.5 px-3.5 pb-3.5 pt-3"> + <div class="flex h-8 w-full flex-none select-none flex-row items-center gap-2 self-stretch px-2.5 pb-2 pt-1"> + <div class="flex h-5 flex-none flex-row items-center p-0 font-[440] text-[13px] leading-5 tracking-[-0.04px] text-v2-text-text-faint [font-family:Inter,var(--font-family-sans)] [font-variant-numeric:tabular-nums] [font-variation-settings:'slnt'_0]"> + {language.t("dialog.model.unpaid.freeModels.title")} + </div> + </div> + <For each={model.list()}> + {(item) => ( + <TooltipV2 + class="w-full" + placement="right-start" + gutter={6} + openDelay={0} + value={<ModelTooltip model={item} latest={item.latest} free={isFree(item)} v2 />} + > + <button + type="button" + class="flex w-full scroll-my-3.5 flex-row items-center gap-2 rounded-md px-2.5 py-2 text-left text-[13px] font-[530] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-family:Inter,var(--font-family-sans)] [font-variation-settings:'slnt'_0] hover:bg-v2-overlay-simple-overlay-hover focus:bg-v2-overlay-simple-overlay-hover focus:outline-none" + onClick={() => selectModel(item)} + > + <span class="min-w-0 truncate">{item.name}</span> + <Show when={isFree(item)}> + <Tag class="shrink-0">{language.t("model.tag.free")}</Tag> + </Show> + <Show when={item.latest}> + <Tag class="shrink-0">{language.t("model.tag.latest")}</Tag> + </Show> + <Show when={currentKey() === modelKey(item)}> + <Icon name="check" class="ml-auto size-4 shrink-0 text-v2-icon-icon-base" /> + </Show> + </button> + </TooltipV2> + )} + </For> + </div> + + <div class="flex w-full flex-col p-2.5 pt-0"> + <div class="flex h-fit w-full flex-none grow-0 flex-col items-start gap-0.5 self-stretch rounded-lg bg-v2-background-bg-layer-02 p-1 shadow-[var(--v2-elevation-switch-off)]"> + <div class="flex h-8 w-full flex-none select-none flex-row items-center gap-2 self-stretch px-2.5 py-1.5"> + <div class="flex h-5 flex-none flex-row items-center p-0 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint [font-family:Inter,var(--font-family-sans)] [font-variant-numeric:tabular-nums] [font-variation-settings:'slnt'_0]"> + {language.t("dialog.model.unpaid.addMore.title")} + </div> + </div> + <div class="flex w-full flex-col"> + <For + each={[...providers.popular()].sort((a, b) => { + if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) { + return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) + } + return a.name.localeCompare(b.name) + })} + > + {(provider) => ( + <button + type="button" + class="flex w-full scroll-my-3.5 flex-row items-center gap-2 rounded-[6px] px-2.5 py-2 text-left text-[13px] font-[530] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-family:Inter,var(--font-family-sans)] [font-variation-settings:'slnt'_0] hover:bg-v2-overlay-simple-overlay-hover focus:bg-v2-overlay-simple-overlay-hover focus:outline-none" + onClick={() => openProviders(provider.id)} + > + <ProviderIcon id={provider.id} class="size-4 shrink-0 text-v2-icon-icon-muted" /> + <span class="min-w-0 truncate">{provider.name}</span> + <Show when={provider.id === "opencode"}> + <span class="min-w-0 truncate text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted [font-family:Inter,var(--font-family-sans)] [font-variation-settings:'slnt'_0]"> + {language.t("dialog.provider.opencode.tagline")} + </span> + <Tag class="shrink-0">{language.t("dialog.provider.tag.recommended")}</Tag> + </Show> + <Show when={provider.id === "opencode-go"}> + <span class="min-w-0 truncate text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted [font-family:Inter,var(--font-family-sans)] [font-variation-settings:'slnt'_0]"> + {language.t("dialog.provider.opencodeGo.tagline")} + </span> + <Tag class="shrink-0">{language.t("dialog.provider.tag.recommended")}</Tag> + </Show> + <Show when={provider.id === "anthropic"}> + <span class="min-w-0 truncate text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted [font-family:Inter,var(--font-family-sans)] [font-variation-settings:'slnt'_0]"> + {language.t("dialog.provider.anthropic.note")} + </span> + </Show> + </button> + )} + </For> + <button + type="button" + class="flex h-9 w-full scroll-my-3.5 flex-row items-center justify-start gap-2 rounded-[6px] px-2.5 py-2 text-left text-[13px] font-[530] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-family:Inter,var(--font-family-sans)] [font-variation-settings:'slnt'_0] hover:bg-v2-overlay-simple-overlay-hover focus:bg-v2-overlay-simple-overlay-hover focus:outline-none" + onClick={() => openProviders()} + > + <span class="flex size-4 shrink-0 items-center justify-center text-v2-icon-icon-muted"> + <Icon name="dot-grid" size="small" /> + </span> + <span class="min-w-0 truncate text-left text-[13px] font-[530] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-family:Inter,var(--font-family-sans)] [font-variation-settings:'slnt'_0]"> + {language.t("dialog.provider.viewAll")} + </span> + </button> + </div> + </div> + </div> + </div> + </ScrollView> + </DialogBody> + </DialogV2> + ) +} diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 23857946d0..4fb7891ec9 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -474,7 +474,7 @@ export function ModelSelectorPopoverV2(props: { }} onSelect={() => selectModel(item)} > - <span class="min-w-0 truncate">{item.name}</span> + <span class="min-w-0 truncate leading-5">{item.name}</span> <Show when={isFree(item.provider.id, item.cost)}> <TagV2 class="shrink-0">{language.t("model.tag.free")}</TagV2> </Show> diff --git a/packages/app/src/components/help-button.tsx b/packages/app/src/components/help-button.tsx index 4dbfb752a1..b0561b13ce 100644 --- a/packages/app/src/components/help-button.tsx +++ b/packages/app/src/components/help-button.tsx @@ -1,54 +1,144 @@ -import { Icon } from "@opencode-ai/ui/v2/icon" -import { Popover } from "@opencode-ai/ui/popover" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { createSignal, Show } from "solid-js" import { createStore } from "solid-js/store" +import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer" +import { usePlatform } from "@/context/platform" +import introducingTabsVideo from "@/assets/help/introducing-tabs.mp4" +import { Persist, persisted } from "@/utils/persist" + +const helpIcon = ( + <svg + width="16" + height="16" + viewBox="0 0 16 16" + fill="none" + xmlns="http://www.w3.org/2000/svg" + aria-hidden="true" + data-slot="icon-svg" + > + <path + d="M6.94235 10.5714V10.4854C6.94617 9.76302 7.01879 9.18777 7.16022 8.75968C7.30546 8.33158 7.50804 7.98567 7.76796 7.72193C8.02787 7.45819 8.34321 7.21548 8.71397 6.99379C8.93948 6.85619 9.14206 6.69374 9.32171 6.50645C9.50518 6.31916 9.64851 6.10511 9.75171 5.86431C9.85874 5.62351 9.91225 5.35404 9.91225 5.0559C9.91225 4.69661 9.82625 4.38509 9.65424 4.12136C9.48607 3.85762 9.26055 3.65504 8.9777 3.51362C8.69486 3.36837 8.38143 3.29575 8.03743 3.29575C7.73165 3.29575 7.43733 3.35882 7.15448 3.48495C6.87546 3.61108 6.6423 3.80984 6.45501 4.08122C6.26772 4.3526 6.15878 4.70425 6.12821 5.13617H4.56299C4.59357 4.47109 4.76557 3.9054 5.07899 3.43908C5.39242 2.96894 5.80522 2.61156 6.31741 2.36694C6.83341 2.12231 7.40675 2 8.03743 2C8.72161 2 9.31789 2.13378 9.82625 2.40134C10.3384 2.66507 10.734 3.0301 11.0131 3.49642C11.2959 3.96273 11.4373 4.49976 11.4373 5.1075C11.4373 5.53177 11.3724 5.914 11.2424 6.25418C11.1124 6.59436 10.9251 6.89823 10.6805 7.16579C10.4397 7.43335 10.1492 7.67033 9.80905 7.87673C9.48033 8.08313 9.21468 8.301 9.0121 8.53034C8.80952 8.75585 8.66237 9.02341 8.57063 9.33302C8.4789 9.64262 8.42921 10.0268 8.42156 10.4854V10.5714H6.94235ZM7.72782 14C7.43351 14 7.17933 13.8949 6.96528 13.6847C6.75506 13.4744 6.64994 13.2203 6.64994 12.9221C6.64994 12.6278 6.75506 12.3755 6.96528 12.1653C7.17933 11.9551 7.43351 11.85 7.72782 11.85C8.02214 11.85 8.27441 11.9551 8.48463 12.1653C8.69868 12.3755 8.8057 12.6278 8.8057 12.9221C8.8057 13.1209 8.75601 13.3024 8.65663 13.4668C8.55726 13.6273 8.4273 13.7573 8.26676 13.8567C8.10623 13.9522 7.92658 14 7.72782 14Z" + fill="var(--v2-icon-icon-base)" + /> + </svg> +) + +const triggerClass = + "size-7 !rounded-full shrink-0 bg-v2-background-bg-base shadow-[var(--v2-elevation-button-neutral)]" + +// TODO: wire to changelog / seen-state when available +const showPopover = () => true export function HelpButton() { if (import.meta.env.VITE_CODEV_CHANNEL !== "dev") return null - const [state, setState] = /* persisted(Persist.global("help-button"), */ createStore({ dismissed: false }) /* ) */ - const [shown, setShown] = createSignal(false) + const platform = usePlatform() return ( - <Show when={!state.dismissed}> - <div class="fixed bottom-4 right-4 z-50 hidden md:block"> - <Popover - open={shown()} - onOpenChange={setShown} - triggerAs="button" - triggerProps={{ - type: "button", - "aria-label": "Help", - class: - "size-7 rounded-full bg-background-base shadow-[var(--shadow-lg-border-base)] flex items-center justify-center text-text-base hover:text-text-strong transition-colors", - }} - trigger={<span aria-hidden="true">?</span>} - class="[&_[data-slot=popover-body]]:p-0 w-[320px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl" - gutter={8} - placement="top-end" + <a + href="https://opencode.ai" + aria-label="Open the OpenCode website" + data-component="icon-button-v2" + data-size="large" + class={`${triggerClass} fixed bottom-5 right-5 z-50 flex items-center justify-center`} + onClick={(event) => { + event.preventDefault() + platform.openLink(event.currentTarget.href) + }} + > + {helpIcon} + </a> + ) +} + +// can remove this after the tabs rollout has been out for a while +export function TabsInfoPopup() { + if (import.meta.env.VITE_CODEV_CHANNEL !== "dev") return null + + const [state, setState] = persisted(Persist.global("tabsInfoPopup"), createStore({ dismissed: false })) + // setState({ dismissed: false }) // for testing + const [drawerOpen, setDrawerOpen] = createSignal(false) + + return ( + <Drawer open={drawerOpen()} onOpenChange={setDrawerOpen} side="right"> + <Show when={!state.dismissed}> + <div + class="fixed bottom-14 right-5 z-50 h-[240px] w-[192px] rounded-[8px] bg-v2-background-bg-base p-1 shadow-[var(--v2-elevation-floating)]" + aria-label="Introducing Tabs. A faster, more intuitive way to work." > - <Show when={shown()}> - <div class="relative flex flex-col gap-1 w-[320px] p-4 rounded-xl bg-background-strong shadow-[var(--shadow-lg-border-base)]"> - <button - type="button" - aria-label="Close" - class="absolute top-3.5 right-3.5 size-6 rounded-md flex items-center justify-center text-text-base hover:text-text-strong hover:bg-surface-raised-base-hover transition-colors" - onClick={() => { - setShown(false) - setState("dismissed", true) - }} - > - <Icon name="xmark-small" /> - </button> - <span class="text-14-regular text-text-strong">Lorem ipsum dolor sit amet</span> - <p class="text-12-regular text-text-weak"> - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et - dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation. + <button + type="button" + aria-label="Dismiss Tabs information" + class="absolute top-3 right-3 z-10 size-5 flex items-center justify-center rounded-[4px] bg-[rgba(0,0,0,0.4)]" + onClick={() => setState("dismissed", true)} + > + <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.25M11.75 11.75L4.25 4.25" stroke="white" /> + </svg> + </button> + <button + type="button" + class="relative block h-[232px] w-[184px] cursor-pointer overflow-hidden rounded-[4px] text-left" + onClick={() => { + setState("dismissed", true) + setDrawerOpen(true) + }} + > + <video + src={introducingTabsVideo} + class="absolute inset-0 h-full w-full object-cover" + loop + muted + autoplay + playsinline + aria-hidden="true" + onContextMenu={(event) => event.preventDefault()} + /> + <div class="absolute inset-x-0 bottom-0 flex w-full flex-col items-start gap-1.5 bg-[linear-gradient(180deg,rgba(0,0,0,0)_0%,#000000_100%)] px-3 py-5"> + <p class="w-full select-none text-[13px] font-[530] leading-none tracking-[-0.04px] text-[#FFFFFF]"> + Introducing Tabs + </p> + <p class="w-full select-none text-[13px] font-[440] leading-[140%] tracking-[-0.04px] text-[#808080]"> + A faster, more intuitive way to work. </p> </div> - </Show> - </Popover> - </div> - </Show> + </button> + </div> + </Show> + <DrawerContent> + <div class="flex h-[52px] w-full shrink-0 items-center gap-4 self-stretch border-b border-v2-border-border-muted p-4"> + <p class="min-h-0 min-w-0 flex-1 text-[13px] font-[530] leading-5 tracking-[-0.04px] tabular-nums text-v2-text-text-muted"> + June 16 + </p> + <DrawerClose + as={IconButtonV2} + type="button" + size="small" + variant="ghost-muted" + aria-label="Close" + icon={<IconV2 name="xmark-small" />} + /> + </div> + <div class="relative flex w-full flex-col items-start gap-6 p-8"> + <p class="w-full shrink-0 self-stretch text-[21px] font-[610] leading-6 tracking-[-0.37px] tabular-nums text-v2-text-text-base"> + Introducing Tabs Navigation. + </p> + <p class="w-full flex-1 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base"> + We've introduced tabs as the primary navigation in OpenCode. Your most important session are now pinned at + the top of your screen at all times. No more hunting through menus or losing your place mid-session. Switch + contexts instantly, pick up exactly where you left off, and keep your focus where it belongs: on the + sessions. + </p> + </div> + </DrawerContent> + </Drawer> ) } diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 92aefd9717..324a789825 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -43,6 +43,8 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Select } from "@opencode-ai/ui/select" import { useDialog } from "@opencode-ai/ui/context/dialog" import { ModelSelectorPopover, ModelSelectorPopoverV2 } from "@/components/dialog-select-model" +import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" +import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2" import { useCommand } from "@/context/command" import { Persist, persisted } from "@/utils/persist" import { usePermission } from "@/context/permission" @@ -1549,9 +1551,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => { style: control(), onClose: restoreFocus, onUnpaidClick: () => { - void import("@/components/dialog-select-model-unpaid").then((x) => { - dialog.show(() => <x.DialogSelectModelUnpaid model={props.controls.model.selection} />) - }) + if (props.controls.newLayoutDesigns) { + dialog.show(() => <DialogSelectModelUnpaidV2 model={props.controls.model.selection} />) + return + } + dialog.show(() => <DialogSelectModelUnpaid model={props.controls.model.selection} />) }, })) @@ -1772,7 +1776,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { class="max-w-[160px] justify-start capitalize" style={control()} > - <span class="truncate"> + <span class="truncate leading-5"> {props.controls.model.selection.variant.current() ?? language.t("common.default")} </span> <span class="-ml-0.5 -mr-1 flex shrink-0"> @@ -2059,11 +2063,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => { class="min-w-0 max-w-[320px] text-13-regular text-text-base group" style={control()} onClick={() => { - void import("@/components/dialog-select-model-unpaid").then((x) => { - dialog.show(() => ( - <x.DialogSelectModelUnpaid model={props.controls.model.selection} /> - )) - }) + dialog.show(() => ( + <DialogSelectModelUnpaid model={props.controls.model.selection} /> + )) }} > <Show when={props.controls.model.selection.current()?.provider?.id}> @@ -2230,30 +2232,47 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) { </> } > - <Button - data-action="prompt-model" - as="div" - variant="ghost" - size="normal" - class="min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-5 text-v2-text-text-faint group" - classList={{ "animate-in fade-in": props.state.shouldAnimate }} - style={props.state.style} - onClick={props.state.onUnpaidClick} + <Show + when={props.state.newLayoutDesigns} + fallback={ + <Button + data-action="prompt-model" + as="div" + variant="ghost" + size="normal" + class="min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-5 text-v2-text-text-faint group" + classList={{ "animate-in fade-in": props.state.shouldAnimate }} + style={props.state.style} + onClick={props.state.onUnpaidClick} + > + <Show when={props.state.providerID}> + {(providerID) => ( + <ProviderIcon + id={providerID()} + class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150" + style={{ "will-change": "opacity", transform: "translateZ(0)" }} + /> + )} + </Show> + <span class="truncate">{props.state.modelName}</span> + <span class="-ml-1 shrink-0 flex size-fit"> + <Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" /> + </span> + </Button> + } > - <Show when={props.state.providerID}> - {(providerID) => ( - <ProviderIcon - id={providerID()} - class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150" - style={{ "will-change": "opacity", transform: "translateZ(0)" }} - /> - )} - </Show> - <span class="truncate">{props.state.modelName}</span> - <span class="-ml-1 shrink-0 flex size-fit"> - <Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" /> - </span> - </Button> + <ButtonV2 + data-action="prompt-model" + variant="ghost-muted" + size="normal" + class="min-w-0 max-w-[220px] justify-start ![font-weight:440] group" + classList={{ "animate-in fade-in": props.state.shouldAnimate }} + style={props.state.style} + onClick={props.state.onUnpaidClick} + > + <ModelControlContent state={props.state} v2 /> + </ButtonV2> + </Show> </TooltipV2> } > diff --git a/packages/app/src/components/ui/drawer.tsx b/packages/app/src/components/ui/drawer.tsx new file mode 100644 index 0000000000..9ac555ec9d --- /dev/null +++ b/packages/app/src/components/ui/drawer.tsx @@ -0,0 +1,119 @@ +/** + * Taken from https://www.solid-ui.com/docs/components/drawer + * Only used in one place hence not a v2 component yet... can be promoted to ui/v2 later + */ + +import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js" +import { splitProps } from "solid-js" +import type { ContentProps, DescriptionProps, DynamicProps, LabelProps, OverlayProps } from "@corvu/drawer" +import DrawerPrimitive from "@corvu/drawer" + +const Drawer = DrawerPrimitive + +const DrawerTrigger = DrawerPrimitive.Trigger + +const DrawerPortal = DrawerPrimitive.Portal + +const DrawerClose = DrawerPrimitive.Close + +type DrawerOverlayProps<T extends ValidComponent = "div"> = OverlayProps<T> & { class?: string } + +const DrawerOverlay = <T extends ValidComponent = "div">(props: DynamicProps<T, DrawerOverlayProps<T>>) => { + const [, rest] = splitProps(props as DrawerOverlayProps, ["class"]) + const drawerContext = DrawerPrimitive.useContext() + const overlayStyle = () => { + const state = drawerContext.transitionState() + if (state === "opening" || state === "closing") return undefined + const open = drawerContext.openPercentage() + return { + opacity: open, + "backdrop-filter": `blur(${4 * open}px)`, + } + } + return ( + <DrawerPrimitive.Overlay + class={props.class} + classList={{ + "fixed inset-0 z-[100] bg-v2-overlay-simple-overlay-scrim opacity-0 backdrop-blur-none transition-[opacity,backdrop-filter] duration-300 data-[opening]:opacity-100 data-[opening]:backdrop-blur-[4px] data-[closing]:opacity-0 data-[closing]:backdrop-blur-none": true, + }} + style={overlayStyle()} + {...rest} + /> + ) +} + +type DrawerContentProps<T extends ValidComponent = "div"> = ContentProps<T> & { + class?: string + children?: JSX.Element +} + +const DrawerContent = <T extends ValidComponent = "div">(props: DynamicProps<T, DrawerContentProps<T>>) => { + const [, rest] = splitProps(props as DrawerContentProps, ["class", "children"]) + return ( + <DrawerPortal> + <DrawerOverlay /> + <DrawerPrimitive.Content + class={props.class} + classList={{ + "group/drawer-content fixed inset-y-[6px] right-[6px] left-auto z-[100] flex h-auto max-h-[calc(100vh-12px)] w-[560px] max-w-[calc(100vw-12px)] flex-col items-start rounded-[8px] bg-v2-background-bg-base p-0 shadow-[var(--v2-elevation-overlay)] data-[transitioning]:transition-transform data-[transitioning]:duration-300 md:select-none": true, + }} + {...rest} + > + {props.children} + </DrawerPrimitive.Content> + </DrawerPortal> + ) +} + +const DrawerHeader: Component<ComponentProps<"div">> = (props) => { + const [, rest] = splitProps(props, ["class"]) + return <div class={props.class} classList={{ "grid gap-1.5 p-4 text-center sm:text-left": true }} {...rest} /> +} + +const DrawerFooter: Component<ComponentProps<"div">> = (props) => { + const [, rest] = splitProps(props, ["class"]) + return <div class={props.class} classList={{ "mt-auto flex flex-col gap-2 p-4": true }} {...rest} /> +} + +type DrawerTitleProps<T extends ValidComponent = "div"> = LabelProps<T> & { class?: string } + +const DrawerTitle = <T extends ValidComponent = "div">(props: DynamicProps<T, DrawerTitleProps<T>>) => { + const [, rest] = splitProps(props as DrawerTitleProps, ["class"]) + return ( + <DrawerPrimitive.Label + class={props.class} + classList={{ "text-base font-[530] leading-none tracking-[-0.04px] text-v2-text-text-base": true }} + {...rest} + /> + ) +} + +type DrawerDescriptionProps<T extends ValidComponent = "div"> = DescriptionProps<T> & { + class?: string +} + +const DrawerDescription = <T extends ValidComponent = "div">(props: DynamicProps<T, DrawerDescriptionProps<T>>) => { + const [, rest] = splitProps(props as DrawerDescriptionProps, ["class"]) + return ( + <DrawerPrimitive.Description + class={props.class} + classList={{ + "text-[13px] font-[440] leading-[140%] tracking-[-0.04px] text-v2-text-text-muted": true, + }} + {...rest} + /> + ) +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/packages/app/src/env.d.ts b/packages/app/src/env.d.ts index db3436e689..82aeb4e98e 100644 --- a/packages/app/src/env.d.ts +++ b/packages/app/src/env.d.ts @@ -12,6 +12,16 @@ interface ImportMeta { readonly env: ImportMetaEnv } +declare module "*.png" { + const src: string + export default src +} + +declare module "*.mp4" { + const src: string + export default src +} + export declare module "solid-js" { namespace JSX { interface Directives { diff --git a/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx b/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx new file mode 100644 index 0000000000..dd030870a1 --- /dev/null +++ b/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx @@ -0,0 +1,118 @@ +import { For } from "solid-js" +import { createStore } from "solid-js/store" +import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" +import { SettingsProvider, useSettings } from "@/context/settings" + +export default { + title: "Composer/Revert Dock", + id: "composer-revert-dock", + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: `### Overview +Real \`SessionRevertDock\` from app code, rendered above a mock composer card. + +### Source path +- \`packages/app/src/pages/session/composer/session-revert-dock.tsx\` + +### Why the mock composer +The live composer overlaps the dock's bottom by 18px (\`session-composer-region-controller.ts\` \`lift()\`). +The card below reproduces that overlap so the collapsed/expanded cutoff behavior can be verified in isolation. + +### Layout split +Use the **Layout** button to toggle \`newLayoutDesigns\` and preview both the v2 dock and the legacy (v1) \`DockTray\` fallback. + +### Notes +- \`onRestore\` only mutates local story state, so nothing in the real session is affected. +- Click the header to expand/collapse. Click "Restore message" to remove a row.`, + }, + }, + }, +} + +const messages = [ + "update current branch with latest changes from dev and fix conflicts if any", + "investigate why the chat input loses focus after sending a message", + "Debug why streaming responses sometimes duplicate the last token", + "suggest a better title for this PR based on the diff", + "add a storybook story for the revert dock", + "reduce re-renders in the timeline component", +] + +const btn = (accent?: boolean) => + ({ + padding: "6px 12px", + "border-radius": "6px", + border: "1px solid var(--v2-border-border-base, #0000001a)", + background: accent ? "var(--v2-background-bg-contrast, #242424)" : "var(--v2-background-bg-base, #fff)", + color: accent ? "var(--v2-text-text-contrast, #fafafa)" : "var(--v2-text-text-base, #161616)", + cursor: "pointer", + "font-size": "13px", + }) as const + +function Stage(props: { count: number }) { + const settings = useSettings() + const seed = () => messages.slice(0, props.count).map((text, index) => ({ id: `rolled-${index}`, text })) + const [store, setStore] = createStore({ items: seed() }) + + const v2 = () => settings.general.newLayoutDesigns() + const reset = () => setStore("items", seed()) + const restore = (id: string) => + setStore( + "items", + store.items.filter((item) => item.id !== id), + ) + + return ( + <div style={{ display: "grid", gap: "16px", "max-width": "720px" }}> + <div style={{ display: "flex", gap: "8px" }}> + <button style={btn()} onClick={reset}> + Reset ({props.count}) + </button> + <button style={btn(v2())} onClick={() => settings.general.setNewLayoutDesigns(!v2())}> + Layout: {v2() ? "v2" : "v1"} + </button> + </div> + + {/* Reproduce the real composer stack: dock + card overlapping the dock's bottom by lift() = 18px */} + <div style={{ display: "flex", "flex-direction": "column" }}> + <SessionRevertDock items={store.items} onRestore={restore} /> + <div + style={{ position: "relative", "z-index": 70, "margin-top": "-18px" }} + class="min-h-24 w-full rounded-[12px] border border-v2-border-border-base bg-v2-background-bg-base px-4 py-3 text-[13px] text-v2-text-text-faint" + > + Ask anything... + </div> + </div> + + <div class="text-[12px] text-v2-text-text-faint"> + Restored so far:{" "} + <For each={seed()}> + {(item) => <span>{store.items.some((current) => current.id === item.id) ? "" : `“${item.text}” `}</span>} + </For> + </div> + </div> + ) +} + +const story = (count: number) => () => ( + <SettingsProvider> + <Stage count={count} /> + </SettingsProvider> +) + +export const OneMessage = { + name: "1 rolled back", + render: story(1), +} + +export const ThreeMessages = { + name: "3 rolled back", + render: story(3), +} + +export const ManyMessages = { + name: "6 rolled back (scrolls)", + render: story(6), +} diff --git a/packages/app/src/pages/session/composer/session-revert-dock.tsx b/packages/app/src/pages/session/composer/session-revert-dock.tsx index 5a5b8a9cd4..d8f022a537 100644 --- a/packages/app/src/pages/session/composer/session-revert-dock.tsx +++ b/packages/app/src/pages/session/composer/session-revert-dock.tsx @@ -3,7 +3,11 @@ import { createStore } from "solid-js/store" import { Button } from "@opencode-ai/ui/button" import { DockTray } from "@opencode-ai/ui/dock-surface" import { IconButton } from "@opencode-ai/ui/icon-button" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { useLanguage } from "@/context/language" +import { useSettings } from "@/context/settings" export function SessionRevertDock(props: { items: { id: string; text: string }[] @@ -12,6 +16,7 @@ export function SessionRevertDock(props: { onRestore: (id: string) => void }) { const language = useLanguage() + const settings = useSettings() const [store, setStore] = createStore({ collapsed: true, }) @@ -31,69 +36,153 @@ export function SessionRevertDock(props: { ) const preview = createMemo(() => props.items[0]?.text ?? "") + const onHeaderKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Enter" && event.key !== " ") return + event.preventDefault() + toggle() + } + return ( - <DockTray data-component="session-revert-dock"> + <Show + when={settings.general.newLayoutDesigns()} + fallback={ + <DockTray data-component="session-revert-dock"> + <div + class="pl-3 pr-2 py-2 flex items-center gap-2" + role="button" + tabIndex={0} + onClick={toggle} + onKeyDown={onHeaderKeyDown} + > + <span class="shrink-0 text-14-regular text-text-strong cursor-default">{label()}</span> + <Show when={store.collapsed && preview()}> + <span class="min-w-0 flex-1 truncate text-14-regular text-text-base cursor-default">{preview()}</span> + </Show> + <div class="ml-auto shrink-0"> + <IconButton + icon="chevron-down" + size="normal" + variant="ghost" + style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }} + onMouseDown={(event) => { + event.preventDefault() + event.stopPropagation() + }} + onClick={(event) => { + event.stopPropagation() + toggle() + }} + aria-label={ + store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse") + } + /> + </div> + </div> + + <Show when={store.collapsed}> + <div class="h-5" aria-hidden="true" /> + </Show> + + <Show when={!store.collapsed}> + <div class="px-3 pb-7 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar"> + <For each={props.items}> + {(item) => ( + <div class="flex items-center gap-2 min-w-0 py-1"> + <span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span> + <Button + size="small" + variant="secondary" + class="shrink-0" + disabled={props.disabled || !!props.restoring} + onClick={() => props.onRestore(item.id)} + > + {language.t("session.revertDock.restore")} + </Button> + </div> + )} + </For> + </div> + </Show> + </DockTray> + } + > <div - class="pl-3 pr-2 py-2 flex items-center gap-2" - role="button" - tabIndex={0} - onClick={toggle} - onKeyDown={(event) => { - if (event.key !== "Enter" && event.key !== " ") return - event.preventDefault() - toggle() - }} + data-component="session-revert-dock" + class="w-full overflow-hidden rounded-xl border-[0.5px] border-v2-border-border-base bg-v2-background-bg-layer-01" > - <span class="shrink-0 text-14-regular text-text-strong cursor-default">{label()}</span> - <Show when={store.collapsed && preview()}> - <span class="min-w-0 flex-1 truncate text-14-regular text-text-base cursor-default">{preview()}</span> - </Show> - <div class="ml-auto shrink-0"> - <IconButton - data-collapsed={store.collapsed ? "true" : "false"} - icon="chevron-down" - size="normal" - variant="ghost" - style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }} - onMouseDown={(event) => { - event.preventDefault() - event.stopPropagation() - }} - onClick={(event) => { - event.stopPropagation() - toggle() + <div + class="flex h-[42px] items-center gap-2 pl-4 pr-2" + role="button" + tabIndex={0} + onClick={toggle} + onKeyDown={onHeaderKeyDown} + > + <IconV2 name="outline-reset" size="normal" class="text-v2-icon-icon-muted" /> + <span + classList={{ + "font-[440] shrink-0 cursor-default text-[13px] leading-5 tracking-[-0.04px]": true, + "text-v2-text-text-base": !store.collapsed, + "text-v2-text-text-muted": store.collapsed, }} - aria-label={ - store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse") - } - /> + > + {label()} + </span> + <Show when={store.collapsed && preview()}> + <span class="min-w-0 flex-1 truncate cursor-default text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint"> + {preview()} + </span> + </Show> + <div class="ml-auto shrink-0"> + <IconButtonV2 + icon={<IconV2 name="outline-chevron-down" size="small" />} + size="large" + variant="ghost-muted" + style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }} + onMouseDown={(event) => { + event.preventDefault() + event.stopPropagation() + }} + onClick={(event) => { + event.stopPropagation() + toggle() + }} + aria-label={ + store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse") + } + /> + </div> </div> - </div> - <Show when={store.collapsed}> - <div class="h-5" aria-hidden="true" /> - </Show> + {/* Sacrificial space the composer overlaps via its negative lift (18px), so the header stays fully visible */} + <Show when={store.collapsed}> + <div class="h-[18px]" aria-hidden="true" /> + </Show> - <Show when={!store.collapsed}> - <div class="px-3 pb-7 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar"> - <For each={props.items}> - {(item) => ( - <div class="flex items-center gap-2 min-w-0 py-1"> - <span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span> - <Button - size="small" - variant="secondary" - class="shrink-0" - disabled={props.disabled || !!props.restoring} - onClick={() => props.onRestore(item.id)} - > - {language.t("session.revertDock.restore")} - </Button> - </div> - )} - </For> - </div> - </Show> - </DockTray> + <Show when={!store.collapsed}> + {/* Scroll viewport ends above the composer; the 18px sacrificial below is what the composer overlaps */} + <div class="flex max-h-42 flex-col gap-2 overflow-y-auto px-4 pt-px pb-3 no-scrollbar"> + <For each={props.items}> + {(item) => ( + <div class="flex h-6 min-w-0 items-center gap-2"> + <span class="min-w-0 flex-1 truncate text-[13px] font-[400] leading-5 tracking-[-0.04px] text-v2-text-text-muted"> + {item.text} + </span> + <ButtonV2 + size="small" + variant="neutral" + class="shrink-0" + disabled={props.disabled || !!props.restoring} + onClick={() => props.onRestore(item.id)} + > + {language.t("session.revertDock.restore")} + </ButtonV2> + </div> + )} + </For> + </div> + <div class="h-[18px]" aria-hidden="true" /> + </Show> + </div> + </Show> ) } diff --git a/packages/cli/package.json b/packages/cli/package.json index d3b9981d1a..dd660aa2ee 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.17.16", + "version": "1.17.18", "type": "module", "license": "MIT", "bin": { diff --git a/packages/codemode/package.json b/packages/codemode/package.json index 49bd6edac5..263daa6a53 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "1.17.16", + "version": "1.17.18", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index aa55ac720f..35d31f9342 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.17.16", + "version": "1.17.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index a255d54262..7a3f70e847 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -86,7 +86,7 @@ export async function handler( type CostInfo = ReturnType<typeof calculateCost> const MAX_FAILOVER_RETRIES = 3 - const MAX_429_RETRIES = 3 + const MAX_RETRYABLE_STATUS_RETRIES = 3 const dict = i18n(localeFromRequest(input.request)) const t = (key: Key, params?: Record<string, string | number>) => resolve(dict[key], params) const ADMIN_WORKSPACES = [ @@ -213,7 +213,7 @@ export async function handler( logger.debug("REQUEST URL: " + reqUrl) logger.debug("REQUEST: " + reqBody.substring(0, 300) + "...") const isNewInference = providerInfo.id.startsWith("console.") || providerInfo.id.startsWith("console-go.") - const res = await fetchWith429Retry( + const res = await fetchWithRetryableStatus( reqUrl, { method: "POST", @@ -246,7 +246,7 @@ export async function handler( // abandoned Console requests do not leave orphaned inference work open. signal: input.request.signal, }, - { count: isNewInference ? MAX_429_RETRIES : 0 }, + { count: isNewInference ? MAX_RETRYABLE_STATUS_RETRIES : 0 }, ) if (isNewInference) { @@ -307,7 +307,7 @@ export async function handler( logger.debug("STATUS: " + res.status + " " + res.statusText) // Handle non-streaming response - if (!isStream || [400, 404, 429].includes(res.status)) { + if (!isStream || [400, 404, 429, 529].includes(res.status)) { const json = await res.json() await rateLimiter?.track() const usage = providerInfo.extractUsage(json) @@ -999,11 +999,11 @@ export async function handler( providerInfo.apiKey = authInfo.provider.credentials } - async function fetchWith429Retry(url: string, options: RequestInit, retry = { count: 0 }) { + async function fetchWithRetryableStatus(url: string, options: RequestInit, retry = { count: 0 }) { const res = await fetch(url, options) - if (res.status === 429 && retry.count < MAX_429_RETRIES) { + if ([429, 529].includes(res.status) && retry.count < MAX_RETRYABLE_STATUS_RETRIES) { await new Promise((resolve) => setTimeout(resolve, Math.pow(2, retry.count) * 500)) - return fetchWith429Retry(url, options, { count: retry.count + 1 }) + return fetchWithRetryableStatus(url, options, { count: retry.count + 1 }) } return res } diff --git a/packages/console/core/package.json b/packages/console/core/package.json index c009a35a80..ffc7a756d2 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.17.16", + "version": "1.17.18", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 69904bea90..47ae1efd7a 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.17.16", + "version": "1.17.18", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 3195265394..d0f16d771f 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.17.16", + "version": "1.17.18", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index ef29aa4064..159a59722b 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.17.16", + "version": "1.17.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index c650c7735a..35faf377a5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.16", + "version": "1.17.18", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index e12ad568b9..ece963cbc2 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.17.16", + "version": "1.17.18", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index e6dde2a1f8..6aebc6f41d 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.16", + "version": "1.17.18", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index 100571826d..117aab177f 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.16", + "version": "1.17.18", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index bfeefc0ec8..63f0dc8780 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.17.16", + "version": "1.17.18", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index eee6e3dcd5..282076fa65 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.17.16", + "version": "1.17.18", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 13c7e04eb1..534ec4bc0f 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.16", + "version": "1.17.18", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index 41cecee3c5..91103fd4d0 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.16", + "version": "1.17.18", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 2c539e995f..2aea210834 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.16-1", + "version": "1.17.18", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/opencode/src/plugin/github-copilot/models.ts b/packages/opencode/src/plugin/github-copilot/models.ts index 4e571103a5..f3f32a0b0b 100644 --- a/packages/opencode/src/plugin/github-copilot/models.ts +++ b/packages/opencode/src/plugin/github-copilot/models.ts @@ -99,7 +99,7 @@ function build(key: string, remote: SelectableItem, url: string, prev?: Model): : undefined const prices = remote.billing?.token_prices // Copilot prices are AIC per billing batch; OpenCode stores USD per million tokens. - const usdPerMillion = prices ? 10_000 / prices.batch_size : 0 + const usdPerMillion = prices && prices.batch_size > 0 ? 10_000 / prices.batch_size : 0 const model: CopilotModel = { id: key, diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c1bd9c045b..57bde3b21d 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -207,6 +207,13 @@ function custom(dep: CustomDep): Record<string, CustomLoader> { }, options: { headerTimeout: OPENAI_HEADER_TIMEOUT_DEFAULT }, }), + meta: () => + Effect.succeed({ + autoload: false, + async getModel(sdk: any, modelID: string, _options?: Record<string, any>) { + return sdk.responses(modelID) + }, + }), xai: () => Effect.succeed({ autoload: false, @@ -1064,11 +1071,17 @@ export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof export function toPublicInfo(provider: Info): Info { return JSON.parse( - JSON.stringify(provider, (_, value) => { - if (typeof value === "function" || typeof value === "symbol" || value === undefined) return undefined - if (typeof value === "bigint") return value.toString() - return value - }), + JSON.stringify( + { + ...provider, + models: Object.fromEntries(Object.entries(provider.models).filter(([, model]) => Schema.is(Model)(model))), + }, + (_, value) => { + if (typeof value === "function" || typeof value === "symbol" || value === undefined) return undefined + if (typeof value === "bigint") return value.toString() + return value + }, + ), ) } diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index c14587ba8a..20c73d0216 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -884,6 +884,18 @@ export function variants(model: Provider.Model): Record<string, Record<string, a ) case "@ai-sdk/amazon-bedrock/mantle": case "@ai-sdk/openai": { + if (model.providerID === "meta") { + return Object.fromEntries( + OPENAI_EFFORTS.map((effort) => [ + effort, + { + reasoningEffort: effort, + reasoningSummary: "auto", + include: INCLUDE_ENCRYPTED_REASONING, + }, + ]), + ) + } // https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai const efforts = openaiReasoningEfforts(model.api.id, model.release_date) return Object.fromEntries( @@ -1128,11 +1140,20 @@ export function options(input: { if ( input.providerOptions?.setCacheKey !== false && - (input.model.providerID === "openai" || input.model.api.npm === "@ai-sdk/xai" || input.providerOptions?.setCacheKey) + (input.model.providerID === "openai" || + input.model.api.npm === "@ai-sdk/openai" || + input.model.api.npm === "@ai-sdk/xai" || + input.providerOptions?.setCacheKey) ) { result["promptCacheKey"] = input.sessionID } + if (input.model.providerID === "meta" && input.model.api.npm === "@ai-sdk/openai") { + result["reasoningEffort"] = "high" + result["reasoningSummary"] = "auto" + result["include"] = INCLUDE_ENCRYPTED_REASONING + } + if (input.model.api.npm === "@ai-sdk/google" || input.model.api.npm === "@ai-sdk/google-vertex") { if (input.model.capabilities.reasoning) { result["thinkingConfig"] = { diff --git a/packages/opencode/src/session/prompt/meta.txt b/packages/opencode/src/session/prompt/meta.txt new file mode 100644 index 0000000000..8f0c926c03 --- /dev/null +++ b/packages/opencode/src/session/prompt/meta.txt @@ -0,0 +1,76 @@ +You are OpenCode, the best coding agent on the planet. +You are based on a large language model trained by Meta MSL named Muse Spark. +When asked who you are, identify yourself as OpenCode powered by Meta Muse Spark by name. +You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. +IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files. +If the user asks for help or wants to give feedback inform them of the following: +- ctrl+p to list available actions +- To give feedback, users should report the issue at + https://github.com/anomalyco/opencode +When the user directly asks about OpenCode (eg. "can OpenCode do...", "does OpenCode have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific OpenCode feature (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from OpenCode docs. The list of available docs is available at https://opencode.ai/docs +# Tone and style +- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked. +- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use GitHub-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification. +- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session. +- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files. +# Professional objectivity +Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if OpenCode honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. +# Task Management +You have access to the TodoWrite tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress. +These tools are also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable. +It is critical that you mark todos as completed as soon as you are done with a task. Do not batch up multiple tasks before marking them as completed. +Examples: +<example> +user: Run the build and fix any type errors +assistant: I'm going to use the TodoWrite tool to write the following items to the todo list: +- Run the build +- Fix any type errors +I'm now going to run the build using Bash. +Looks like I found 10 type errors. I'm going to use the TodoWrite tool to write 10 items to the todo list. +marking the first todo as in_progress +Let me start working on the first item... +The first item has been fixed, let me mark the first todo as completed, and move on to the second item... +.. +.. +</example> +In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build and fixing all errors. +<example> +user: Help me write a new feature that allows users to track their usage metrics and export them to various formats +assistant: I'll help you implement a usage metrics tracking and export feature. Let me first use the TodoWrite tool to plan this task. +Adding the following todos to the todo list: +1. Research existing metrics tracking in the codebase +2. Design the metrics collection system +3. Implement core metrics tracking functionality +4. Create export functionality for different formats +Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that. +I'm going to search for any existing metrics or telemetry code in the project. +I've found some existing telemetry code. Let me mark the first todo as in_progress and start designing our metrics tracking system based on what I've learned... +[Assistant continues implementing the feature step by step, marking todos as in_progress and completed as they go] +</example> +# Doing tasks +The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended: +- Use the TodoWrite tool to plan the task if required +- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear. +# Tool usage policy +- When doing file search, prefer to use the Task tool in order to reduce context usage. +- You should proactively use the Task tool with specialized agents when the task at hand matches the agent's description. +- When WebFetch returns a message about a redirect to a different host, you should immediately make a new WebFetch request with the redirect URL provided in the response. +- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls. +- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple Task tool calls. +- Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit for editing instead of sed/awk, and Write for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead. +- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the Task tool instead of running search commands directly. +<example> +user: Where are errors from the client handled? +assistant: [Uses the Task tool to find the files that handle client errors instead of using Glob or Grep directly] +</example> +<example> +user: What is the codebase structure? +assistant: [Uses the Task tool] +</example> +IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation. +# Code References +When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location. +<example> +user: Where are errors from the client handled? +assistant: Clients are marked as failed in the `connectToServer` function in src/services/process.ts:712. +</example> diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 6e5b83ec31..b94e794265 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -9,6 +9,7 @@ import PROMPT_BEAST from "./prompt/beast.txt" import PROMPT_GEMINI from "./prompt/gemini.txt" import PROMPT_GPT from "./prompt/gpt.txt" import PROMPT_KIMI from "./prompt/kimi.txt" +import PROMPT_META from "./prompt/meta.txt" import PROMPT_CODEX from "./prompt/codex.txt" import PROMPT_TRINITY from "./prompt/trinity.txt" @@ -24,6 +25,7 @@ import { MCP } from "@/mcp" import { PermissionV1 } from "@opencode-ai/core/v1/permission" export function provider(model: Provider.Model) { + if (model.api.id.includes("muse-spark")) return [PROMPT_META] if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3")) return [PROMPT_BEAST] if (model.api.id.includes("gpt")) { diff --git a/packages/opencode/test/cli/run/run-process.test.ts b/packages/opencode/test/cli/run/run-process.test.ts index 67a832b321..0caca237ac 100644 --- a/packages/opencode/test/cli/run/run-process.test.ts +++ b/packages/opencode/test/cli/run/run-process.test.ts @@ -95,12 +95,12 @@ describe("codev run (non-interactive subprocess)", () => { }), ) yield* llm.fail("upstream provider exploded mid-stream") - const result = yield* opencode.run("trigger midstream error", { timeoutMs: 30_000 }) + const result = yield* opencode.run("trigger midstream error", { timeoutMs: 60_000 }) expect(result.exitCode).toBe(0) expect(result.stdout).toBe("partial response\n") expect(result.stderr).not.toContain("upstream provider exploded mid-stream") }), - 60_000, + 120_000, ) // --format json puts one JSON object per line on stdout for each emitted @@ -223,7 +223,7 @@ describe("codev run (non-interactive subprocess)", () => { }), ) yield* llm.fail("provider failed") - const result = yield* opencode.run("fail after output", { format: "json" }) + const result = yield* opencode.run("fail after output", { format: "json", timeoutMs: 60_000 }) const events = opencode.parseJsonEvents(result.stdout) expect(result.exitCode).toBe(0) @@ -238,7 +238,7 @@ describe("codev run (non-interactive subprocess)", () => { expect(events[1]?.part).toEqual(expect.objectContaining({ type: "text", text: "partial json" })) expect(events.at(-1)?.part).toEqual(expect.objectContaining({ type: "step-finish", reason: "unknown" })) }), - 60_000, + 120_000, ) cliIt.concurrent( diff --git a/packages/opencode/test/plugin/github-copilot-models.test.ts b/packages/opencode/test/plugin/github-copilot-models.test.ts index 35bfaa6cb4..e6d89fe01c 100644 --- a/packages/opencode/test/plugin/github-copilot-models.test.ts +++ b/packages/opencode/test/plugin/github-copilot-models.test.ts @@ -187,6 +187,60 @@ test("converts Copilot AIC token prices to USD per million tokens", async () => expect(models["ignored-non-chat-record"]).toBeUndefined() }) +test("uses zero cost when Copilot reports a zero billing batch size", async () => { + globalThis.fetch = mock(() => + Promise.resolve( + new Response( + JSON.stringify({ + data: [ + { + model_picker_enabled: true, + id: "mercury-alpha", + name: "Mercury Alpha", + version: "mercury-alpha-2026-07-09", + billing: { + token_prices: { + batch_size: 0, + default: { + input_price: 0, + output_price: 0, + cache_price: 0, + }, + }, + }, + capabilities: { + family: "mercury", + limits: { + max_context_window_tokens: 128000, + max_output_tokens: 16384, + max_prompt_tokens: 128000, + }, + supports: { + streaming: true, + tool_calls: true, + }, + }, + }, + ], + }), + { status: 200 }, + ), + ), + ) as unknown as typeof fetch + + const model = (await CopilotModels.get("https://api.githubcopilot.com")).models["mercury-alpha"] + + expect(model.cost).toEqual({ + input: 0, + output: 0, + cache: { + read: 0, + write: 0, + }, + }) + expect(JSON.stringify(model)).not.toContain("null") +}) + test("records Copilot advertised responses endpoint for non-GPT model IDs", async () => { globalThis.fetch = mock(() => Promise.resolve( diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 6159ecd4a2..986425fbdf 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1426,6 +1426,32 @@ test("models.dev normalization fills required response fields", () => { expect(model.release_date).toBe("") }) +test("public provider info omits invalid models", () => { + const provider = Provider.fromModelsDevProvider({ + id: "test", + name: "Test", + env: [], + models: { + valid: { + id: "valid", + name: "Valid", + cost: { input: 1, output: 1 }, + limit: { context: 128_000, output: 16_000 }, + }, + }, + } as unknown as ModelsDev.Provider) + provider.models.invalid = { + ...provider.models.valid, + id: ModelV2.ID.make("invalid"), + cost: { ...provider.models.valid.cost, input: Number.NaN }, + } + + const result = Provider.toPublicInfo(provider) + + expect(result.models.valid).toBeDefined() + expect(result.models.invalid).toBeUndefined() +}) + it.instance("model variants are generated for reasoning models", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts index 1484bfd631..d9d47be3df 100644 --- a/packages/opencode/test/session/system.test.ts +++ b/packages/opencode/test/session/system.test.ts @@ -1,10 +1,11 @@ -import { describe, expect } from "bun:test" +import { describe, expect, test } from "bun:test" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Effect, Layer } from "effect" import type { Agent } from "../../src/agent/agent" import { NamedError } from "@opencode-ai/core/util/error" import { Skill } from "../../src/skill" import { Permission } from "../../src/permission" +import type { Provider } from "../../src/provider/provider" import { SystemPrompt } from "../../src/session/system" import { MCP } from "../../src/mcp" import { testEffect } from "../lib/effect" @@ -83,6 +84,12 @@ const it = testEffect( ) describe("session.system", () => { + test("selects the Meta prompt for Muse Spark model IDs", () => { + expect(SystemPrompt.provider({ api: { id: "meta/muse-spark-preview" } } as Provider.Model)[0]).toContain( + "Meta Muse Spark", + ) + }) + it.effect("skills output is sorted by name and stable across calls", () => Effect.gen(function* () { const prompt = yield* SystemPrompt.Service diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 2a09f49960..d93419770e 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.17.16", + "version": "1.17.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index f2a5708b2c..81e184968f 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.17.16", + "version": "1.17.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 676241d1c3..9872b789c3 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.17.16", + "version": "1.17.18", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 1734449d1d..cf3be61f36 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/session-ui", - "version": "1.17.16", + "version": "1.17.18", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/src/components/basic-tool.css b/packages/session-ui/src/components/basic-tool.css index bda3069351..f8f20c9ffa 100644 --- a/packages/session-ui/src/components/basic-tool.css +++ b/packages/session-ui/src/components/basic-tool.css @@ -185,6 +185,14 @@ background-color 0.15s ease, color 0.15s ease; + [data-component="task-tool-surface"] { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1 1 auto; + } + [data-slot="basic-tool-tool-info-structured"] { flex: 1 1 auto; min-width: 0; @@ -250,16 +258,30 @@ body[data-new-layout] { [data-component="task-tool-card"] { - padding: 8px 12px 8px 10px; - border-radius: 8px; + gap: 8px; + padding: 0; + border-radius: 0; border: 0; border-color: transparent; - background: var(--task-agent-background, light-dark(#fafafa, rgba(250, 250, 250, 0.15))); - box-shadow: inset 0 0 0 0.5px var(--task-agent-border, var(--v2-border-border-base)); + background: transparent; + box-shadow: none; + + [data-component="task-tool-surface"] { + padding: 8px 12px; + border-radius: 8px; + background: var(--v2-background-bg-layer-01); + } [data-component="task-tool-spinner"], - [data-component="task-tool-title"] { - color: var(--task-agent-color, var(--text-strong)); + [data-component="task-tool-icon"] { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + [data-component="icon"] { + color: var(--task-agent-color, var(--v2-icon-icon-base)); + } } [data-component="task-tool-title"] { @@ -270,6 +292,7 @@ body[data-new-layout] { font-size: 13px; line-height: 20px; letter-spacing: -0.04px; + color: var(--v2-text-text-base); flex: none; flex-grow: 0; } @@ -281,7 +304,7 @@ body[data-new-layout] { font-size: 13px; line-height: 20px; letter-spacing: -0.04px; - color: light-dark(var(--v2-text-text-base), #fafafa); + color: var(--v2-text-text-base); font-variation-settings: "slnt" 0; flex: none; flex-grow: 0; @@ -294,7 +317,11 @@ body[data-new-layout] { &:hover, &:focus-visible { border-color: transparent; - background: var(--task-agent-background, light-dark(#fafafa, rgba(250, 250, 250, 0.15))); + background: transparent; + + [data-component="task-tool-surface"] { + background: var(--v2-background-bg-layer-01); + } } } } diff --git a/packages/session-ui/src/components/message-part.tsx b/packages/session-ui/src/components/message-part.tsx index 098ecdbd62..1ba6be82c8 100644 --- a/packages/session-ui/src/components/message-part.tsx +++ b/packages/session-ui/src/components/message-part.tsx @@ -371,22 +371,12 @@ const agentTones: Record<string, string> = { plan: "var(--icon-agent-plan-base)", } -const v2AgentTones: Record<string, { color: string; border: string; background: string }> = { - build: { - color: "var(--v2-agent-build-solid)", - border: "var(--v2-agent-build-border)", - background: "var(--v2-agent-build-background)", - }, - explore: { - color: "var(--v2-agent-explore-solid)", - border: "var(--v2-agent-explore-border)", - background: "var(--v2-agent-explore-background)", - }, - plan: { - color: "var(--v2-agent-plan-solid)", - border: "var(--v2-agent-plan-border)", - background: "var(--v2-agent-plan-background)", - }, +const v2AgentTones: Record<string, string> = { + build: "var(--v2-agent-build-solid)", + explore: "var(--v2-agent-explore-solid)", + plan: "var(--v2-agent-plan-solid)", + review: "var(--v2-agent-review-solid)", + writer: "var(--v2-agent-writer-solid)", } const agentThemeColors: Record<string, string> = { @@ -433,19 +423,17 @@ function tone(name: string) { function taskAgent( raw: unknown, list?: readonly { name: string; color?: string }[], -): { name?: string; color?: string; v2Color?: string; border?: string; background?: string } { +): { name?: string; color?: string; v2Color?: string } { if (typeof raw !== "string" || !raw) return {} const key = raw.toLowerCase() const item = list?.find((entry) => entry.name === raw || entry.name.toLowerCase() === key) const v2Tone = item?.color ? undefined : v2AgentTones[key] const color = agentColor(item?.color, agentThemeColors) ?? agentTones[key] ?? tone(key) - const v2Color = agentColor(item?.color, v2AgentThemeColors) ?? v2Tone?.color ?? color + const v2Color = agentColor(item?.color, v2AgentThemeColors) ?? v2Tone ?? color return { name: item?.name ?? `${raw[0]!.toUpperCase()}${raw.slice(1)}`, color, v2Color, - border: v2Tone?.border ?? `color-mix(in srgb, ${v2Color} 48%, transparent)`, - background: v2Tone?.background ?? `color-mix(in srgb, ${v2Color} 12%, transparent)`, } } @@ -1937,8 +1925,6 @@ ToolRegistry.register({ const title = createMemo(() => agent().name ?? i18n.t("ui.tool.agent.default")) const tone = createMemo(() => agent().color) const v2Tone = createMemo(() => agent().v2Color) - const border = createMemo(() => agent().border) - const background = createMemo(() => agent().background) const subtitle = createMemo(() => { const value = typeof props.input.description === "string" && props.input.description @@ -1983,25 +1969,34 @@ ToolRegistry.register({ style={{ "--task-agent-color": v2Tone(), "--task-agent-legacy-color": tone(), - "--task-agent-border": border(), - "--task-agent-background": background(), }} > - <div data-slot="basic-tool-tool-info-structured"> - <div data-slot="basic-tool-tool-info-main"> - <Show when={running()}> - <span data-component="task-tool-spinner" style={{ color: tone() ?? "var(--icon-interactive-base)" }}> - <Show when={newLayout()} fallback={<Spinner />}> - <SessionProgressIndicatorV2 - style={{ color: v2Tone() ?? "light-dark(var(--v2-text-text-base), #ffffff)" }} - /> - </Show> - </span> - </Show> - <span data-component="task-tool-title">{title()}</span> - <Show when={subtitle()}> - <span data-slot="basic-tool-tool-subtitle">{subtitle()}</span> - </Show> + <div data-component="task-tool-surface"> + <div data-slot="basic-tool-tool-info-structured"> + <div data-slot="basic-tool-tool-info-main"> + <Show + when={running()} + fallback={ + <Show when={newLayout()}> + <span data-component="task-tool-icon"> + <Icon name="subagent" size="small" /> + </span> + </Show> + } + > + <span data-component="task-tool-spinner" style={{ color: tone() ?? "var(--icon-interactive-base)" }}> + <Show when={newLayout()} fallback={<Spinner />}> + <SessionProgressIndicatorV2 + style={{ color: v2Tone() ?? "light-dark(var(--v2-text-text-base), #ffffff)" }} + /> + </Show> + </span> + </Show> + <span data-component="task-tool-title">{title()}</span> + <Show when={subtitle()}> + <span data-slot="basic-tool-tool-subtitle">{subtitle()}</span> + </Show> + </div> </div> </div> <Show when={clickable()}> diff --git a/packages/session-ui/src/components/timeline-playground.stories.tsx b/packages/session-ui/src/components/timeline-playground.stories.tsx index 53699094ac..d56fc55c4d 100644 --- a/packages/session-ui/src/components/timeline-playground.stories.tsx +++ b/packages/session-ui/src/components/timeline-playground.stories.tsx @@ -359,6 +359,34 @@ const TOOL_SAMPLES = { title: "Agent (Explore)", metadata: { sessionId: "sub-session-1" }, }, + "task build": { + tool: "task", + input: { description: "Implement the settings page", subagent_type: "build", prompt: "Build it" }, + output: "Implemented the settings page.", + title: "Agent (Build)", + metadata: { sessionId: "sub-session-2" }, + }, + "task plan": { + tool: "task", + input: { description: "Plan the migration strategy", subagent_type: "plan", prompt: "Plan it" }, + output: "Drafted a 4-step migration plan.", + title: "Agent (Plan)", + metadata: { sessionId: "sub-session-3" }, + }, + "task themed": { + tool: "task", + input: { description: "Review the diff for regressions", subagent_type: "review", prompt: "Review it" }, + output: "No regressions found.", + title: "Agent (Review)", + metadata: { sessionId: "sub-session-4" }, + }, + "task fallback": { + tool: "task", + input: { description: "Write release notes", subagent_type: "writer", prompt: "Write them" }, + output: "Release notes drafted.", + title: "Agent (Writer)", + metadata: { sessionId: "sub-session-5" }, + }, webfetch: { tool: "webfetch", input: { url: "https://solidjs.com/docs/latest/api" }, @@ -1216,6 +1244,7 @@ function Playground() { const data = createMemo(() => ({ session: [session()], + agent: [{ name: "build" }, { name: "plan" }, { name: "explore" }, { name: "review" }], session_status: {}, session_diff: {}, message: { [session().id]: state.messages }, @@ -1342,6 +1371,19 @@ function Playground() { ]) } + const addAgentTasksTurn = () => { + addFullTurn("Run subagents with every agent type", [ + toolPart(TOOL_SAMPLES.task), + toolPart(TOOL_SAMPLES["task build"]), + toolPart(TOOL_SAMPLES["task plan"]), + toolPart(TOOL_SAMPLES["task themed"]), + toolPart(TOOL_SAMPLES["task fallback"]), + toolPart(TOOL_SAMPLES.task, "running"), + toolPart(TOOL_SAMPLES["task build"], "running"), + toolPart(TOOL_SAMPLES["task plan"], "running"), + ]) + } + const addKitchenSink = () => { // User message variants addUser("short") @@ -1727,6 +1769,9 @@ function Playground() { <button style={btnStyle} onClick={addReasoningFullTurn}> full turn </button> + <button style={btnStyle} onClick={addAgentTasksTurn}> + agent tasks + </button> <button style={btnAccent} onClick={addKitchenSink}> kitchen sink </button> @@ -1966,7 +2011,7 @@ function Playground() { {/* Main area: timeline preview */} <div ref={previewRef!} - style={{ flex: "1", overflow: "auto", "min-width": "0", "background-color": "var(--background-stronger)" }} + style={{ flex: "1", overflow: "auto", "min-width": "0", "background-color": "var(--v2-background-bg-base)" }} > <DataProvider data={data()} directory="/project"> <FileComponentProvider component={FileStub}> diff --git a/packages/slack/package.json b/packages/slack/package.json index ebef44433f..11bb354f0d 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.17.16", + "version": "1.17.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index 811123b5e0..f1def44cec 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.17.16", + "version": "1.17.18", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index 47e55ec306..6733366530 100644 --- a/packages/stats/core/package.json +++ b/packages/stats/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-core", - "version": "1.17.16", + "version": "1.17.18", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index c1f0f7045c..a4d44b874d 100644 --- a/packages/stats/server/package.json +++ b/packages/stats/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-server", - "version": "1.17.16", + "version": "1.17.18", "private": true, "type": "module", "license": "MIT", diff --git a/packages/storybook/.storybook/mocks/app/context/language.ts b/packages/storybook/.storybook/mocks/app/context/language.ts index df28d79fbd..2510acc6f9 100644 --- a/packages/storybook/.storybook/mocks/app/context/language.ts +++ b/packages/storybook/.storybook/mocks/app/context/language.ts @@ -2,6 +2,11 @@ const dict: Record<string, string> = { "session.todo.title": "Todos", "session.todo.collapse": "Collapse todos", "session.todo.expand": "Expand todos", + "session.revertDock.summary.one": "{{count}} rolled back message", + "session.revertDock.summary.other": "{{count}} rolled back messages", + "session.revertDock.collapse": "Collapse rolled back messages", + "session.revertDock.expand": "Expand rolled back messages", + "session.revertDock.restore": "Restore message", "prompt.loading": "Loading prompt...", "prompt.placeholder.normal": "Ask anything...", "prompt.placeholder.simple": "Ask anything...", diff --git a/packages/storybook/.storybook/mocks/app/hooks/use-providers.ts b/packages/storybook/.storybook/mocks/app/hooks/use-providers.ts index 04bd2b485c..caa5e759a0 100644 --- a/packages/storybook/.storybook/mocks/app/hooks/use-providers.ts +++ b/packages/storybook/.storybook/mocks/app/hooks/use-providers.ts @@ -1,5 +1,16 @@ const model_id = "claude-3-7-sonnet" +export const popularProviders = [ + "opencode", + "opencode-go", + "anthropic", + "github-copilot", + "openai", + "google", + "openrouter", + "vercel", +] + const provider = { id: "anthropic", models: { diff --git a/packages/tui/package.json b/packages/tui/package.json index d7107794f6..528f98344b 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "1.17.16", + "version": "1.17.18", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index f521ec861a..26ffadbcf6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.17.16", + "version": "1.17.18", "type": "module", "license": "MIT", "repository": { diff --git a/packages/ui/src/components/icon.stories.tsx b/packages/ui/src/components/icon.stories.tsx index 1986d74772..60c5312988 100644 --- a/packages/ui/src/components/icon.stories.tsx +++ b/packages/ui/src/components/icon.stories.tsx @@ -68,6 +68,7 @@ const names = [ "magnifying-glass-menu", "window-cursor", "task", + "subagent", "stop", "layout-left", "layout-left-partial", diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 7bd461f110..196635269a 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -52,6 +52,7 @@ const icons = { "magnifying-glass-menu": `<path d="M2.08325 10.0002H4.58325M2.08325 5.41683H5.41659M2.08325 14.5835H5.41659M16.4583 13.9585L18.7499 16.2502M17.9166 10.0002C17.9166 12.9917 15.4915 15.4168 12.4999 15.4168C9.50838 15.4168 7.08325 12.9917 7.08325 10.0002C7.08325 7.00862 9.50838 4.5835 12.4999 4.5835C15.4915 4.5835 17.9166 7.00862 17.9166 10.0002Z" stroke="currentColor" stroke-linecap="square"/>`, "window-cursor": `<path d="M17.9166 10.4167V3.75H2.08325V17.0833H10.4166M17.9166 13.5897L11.6666 11.6667L13.5897 17.9167L15.032 15.0321L17.9166 13.5897Z" stroke="currentColor" stroke-width="1.07143" stroke-linecap="square"/><path d="M5.00024 6.125C5.29925 6.12518 5.54126 6.36795 5.54126 6.66699C5.54108 6.96589 5.29914 7.20783 5.00024 7.20801C4.7012 7.20801 4.45843 6.966 4.45825 6.66699C4.45825 6.36784 4.70109 6.125 5.00024 6.125ZM7.91626 6.125C8.21541 6.125 8.45825 6.36784 8.45825 6.66699C8.45808 6.966 8.21531 7.20801 7.91626 7.20801C7.61736 7.20783 7.37542 6.96589 7.37524 6.66699C7.37524 6.36795 7.61726 6.12518 7.91626 6.125ZM10.8333 6.125C11.1324 6.125 11.3752 6.36784 11.3752 6.66699C11.3751 6.966 11.1323 7.20801 10.8333 7.20801C10.5342 7.20801 10.2914 6.966 10.2913 6.66699C10.2913 6.36784 10.5341 6.125 10.8333 6.125Z" fill="currentColor" stroke="currentColor" stroke-width="0.25" stroke-linecap="square"/>`, task: `<path d="M9.99992 2.0835V17.9168M7.08325 3.75016H2.08325V16.2502H7.08325M12.9166 16.2502H17.9166V3.75016H12.9166" stroke="currentColor" stroke-linecap="square"/>`, + subagent: `<path d="M4.5 5C4.5 4.72386 4.72386 4.5 5 4.5H11C11.2761 4.5 11.5 4.72386 11.5 5V11C11.5 11.2761 11.2761 11.5 11 11.5H5C4.72386 11.5 4.5 11.2761 4.5 11V5Z" fill="currentColor"/><path d="M13.5 2C13.7761 2 14 2.22386 14 2.5V13.5C14 13.7761 13.7761 14 13.5 14H2.5C2.22386 14 2 13.7761 2 13.5V2.5C2 2.22386 2.22386 2 2.5 2H13.5ZM3 13H13V3H3V13Z" fill="currentColor"/>`, stop: `<rect x="5" y="5" width="10" height="10" fill="currentColor"/>`, status: `<path d="M2 10V18H18V10M2 10V2H18V10M2 10H18M5 6H9M5 14H9" stroke="currentColor"/>`, "status-active": `<path d="M18 2H2V10H18V2Z" fill="currentColor" fill-opacity="0.1"/> @@ -110,7 +111,7 @@ const symbol = (name: keyof typeof icons) => `opencode-icon-${name}` let spriteInserted = false function viewBox(name: keyof typeof icons) { - return name === "magnifying-glass" || name === "arrow-undo-down" ? "0 0 16 16" : "0 0 20 20" + return name === "magnifying-glass" || name === "arrow-undo-down" || name === "subagent" ? "0 0 16 16" : "0 0 20 20" } function ensureSprite() { diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css index 894f65e45d..b652f9610d 100644 --- a/packages/ui/src/styles/theme.css +++ b/packages/ui/src/styles/theme.css @@ -281,15 +281,17 @@ --icon-agent-docs-base: #fcb239; --icon-agent-ask-base: #2090f5; --icon-agent-build-base: #034cff; - --v2-agent-plan-solid: var(--v2-pink-900); - --v2-agent-plan-border: rgba(200, 61, 139, 0.5); - --v2-agent-plan-background: rgba(253, 236, 243, 0.33); - --v2-agent-build-solid: var(--v2-blue-900); - --v2-agent-build-border: rgba(59, 92, 246, 0.5); - --v2-agent-build-background: rgba(236, 241, 254, 0.33); + --v2-agent-plan-solid: var(--v2-pink-800); + --v2-agent-plan-border: rgba(200, 61, 139, 0.2); + --v2-agent-plan-background: rgba(253, 236, 243, 0.1); + --v2-agent-build-solid: var(--v2-blue-800); + --v2-agent-build-border: rgba(44, 71, 200, 0.1); + --v2-agent-build-background: rgba(236, 241, 254, 0.1); --v2-agent-explore-solid: var(--v2-yellow-900); - --v2-agent-explore-border: rgba(142, 114, 49, 0.5); - --v2-agent-explore-background: rgba(254, 250, 236, 0.33); + --v2-agent-explore-border: rgba(203, 159, 52, 0.2); + --v2-agent-explore-background: rgba(254, 250, 236, 0.1); + --v2-agent-review-solid: var(--v2-green-800); + --v2-agent-writer-solid: var(--v2-purple-700); --icon-on-success-base: rgba(18, 201, 5, 0.9); --icon-on-success-hover: rgba(45, 186, 38, 0.9); --icon-on-success-selected: rgba(7, 137, 1, 0.9); @@ -551,14 +553,16 @@ --icon-agent-ask-base: #2090f5; --icon-agent-build-base: #9dbefe; --v2-agent-plan-solid: var(--v2-pink-400); - --v2-agent-plan-border: rgba(250, 188, 216, 0.5); - --v2-agent-plan-background: rgba(247, 213, 228, 0.1); - --v2-agent-build-solid: var(--v2-blue-400); - --v2-agent-build-border: rgba(215, 226, 252, 0.5); - --v2-agent-build-background: rgba(215, 226, 252, 0.1); - --v2-agent-explore-solid: var(--v2-yellow-400); - --v2-agent-explore-border: rgba(247, 229, 181, 0.5); - --v2-agent-explore-background: rgba(252, 239, 208, 0.1); + --v2-agent-plan-border: rgba(247, 153, 198, 0.2); + --v2-agent-plan-background: rgba(170, 53, 118, 0.05); + --v2-agent-build-solid: var(--v2-blue-300); + --v2-agent-build-border: rgba(162, 188, 255, 0.2); + --v2-agent-build-background: rgba(38, 63, 169, 0.05); + --v2-agent-explore-solid: var(--v2-yellow-300); + --v2-agent-explore-border: rgba(243, 218, 155, 0.2); + --v2-agent-explore-background: rgba(172, 136, 51, 0.05); + --v2-agent-review-solid: var(--v2-green-300); + --v2-agent-writer-solid: var(--v2-purple-400); --icon-on-success-base: rgba(18, 201, 5, 0.9); --icon-on-success-hover: rgba(53, 192, 45, 0.9); --icon-on-success-selected: rgba(77, 225, 68, 0.9); diff --git a/packages/ui/src/theme/v2/mapping.ts b/packages/ui/src/theme/v2/mapping.ts index 9bb29fbfa9..e019fa466e 100644 --- a/packages/ui/src/theme/v2/mapping.ts +++ b/packages/ui/src/theme/v2/mapping.ts @@ -4,27 +4,31 @@ import { V2_AVATAR_DARK, V2_AVATAR_LIGHT } from "./avatar" const ref = (name: string): V2ColorValue => `var(--${name})` const lightAgentTokens: Record<string, V2ColorValue> = { - "v2-agent-plan-solid": ref("v2-pink-900"), - "v2-agent-plan-border": "rgba(200, 61, 139, 0.5)", - "v2-agent-plan-background": "rgba(253, 236, 243, 0.33)", - "v2-agent-build-solid": ref("v2-blue-900"), - "v2-agent-build-border": "rgba(59, 92, 246, 0.5)", - "v2-agent-build-background": "rgba(236, 241, 254, 0.33)", + "v2-agent-plan-solid": ref("v2-pink-800"), + "v2-agent-plan-border": "rgba(200, 61, 139, 0.20)", + "v2-agent-plan-background": "rgba(253, 236, 243, 0.10)", + "v2-agent-build-solid": ref("v2-blue-800"), + "v2-agent-build-border": "rgba(44, 71, 200, 0.20)", + "v2-agent-build-background": "rgba(236, 241, 254, 0.10)", "v2-agent-explore-solid": ref("v2-yellow-900"), - "v2-agent-explore-border": "rgba(142, 114, 49, 0.5)", - "v2-agent-explore-background": "rgba(254, 250, 236, 0.33)", + "v2-agent-explore-border": "rgba(203, 159, 52, 0.20)", + "v2-agent-explore-background": "rgba(254, 250, 236, 0.1)", + "v2-agent-review-solid": ref("v2-green-800"), + "v2-agent-writer-solid": ref("v2-purple-700"), } const darkAgentTokens: Record<string, V2ColorValue> = { "v2-agent-plan-solid": ref("v2-pink-400"), - "v2-agent-plan-border": "rgba(250, 188, 216, 0.5)", - "v2-agent-plan-background": "rgba(247, 213, 228, 0.1)", - "v2-agent-build-solid": ref("v2-blue-400"), - "v2-agent-build-border": "rgba(215, 226, 252, 0.5)", - "v2-agent-build-background": "rgba(215, 226, 252, 0.1)", - "v2-agent-explore-solid": ref("v2-yellow-400"), - "v2-agent-explore-border": "rgba(247, 229, 181, 0.5)", - "v2-agent-explore-background": "rgba(252, 239, 208, 0.1)", + "v2-agent-plan-border": "rgba(247, 153, 198, 0.20)", + "v2-agent-plan-background": "rgba(170, 53, 118, 0.05)", + "v2-agent-build-solid": ref("v2-blue-300"), + "v2-agent-build-border": "rgba(162, 188, 255, 0.20)", + "v2-agent-build-background": "rgba(38, 63, 169, 0.05)", + "v2-agent-explore-solid": ref("v2-yellow-300"), + "v2-agent-explore-border": "rgba(243, 218, 155, 0.20)", + "v2-agent-explore-background": "rgba(172, 136, 51, 0.05)", + "v2-agent-review-solid": ref("v2-green-300"), + "v2-agent-writer-solid": ref("v2-purple-400"), } const light: Record<string, V2ColorValue> = { diff --git a/packages/ui/src/v2/components/button-v2.css b/packages/ui/src/v2/components/button-v2.css index 8f7b475610..97f38e6b8b 100644 --- a/packages/ui/src/v2/components/button-v2.css +++ b/packages/ui/src/v2/components/button-v2.css @@ -14,7 +14,7 @@ font-style: normal; font-weight: 530; font-size: 13px; - line-height: 1; + line-height: 20px; color: var(--v2-text-text-base); text-shadow: none; font-variant-numeric: tabular-nums; diff --git a/packages/ui/src/v2/components/dialog-v2.tsx b/packages/ui/src/v2/components/dialog-v2.tsx index 06821f0ffe..48a6f53712 100644 --- a/packages/ui/src/v2/components/dialog-v2.tsx +++ b/packages/ui/src/v2/components/dialog-v2.tsx @@ -6,6 +6,7 @@ export interface DialogProps extends ParentProps { size?: "normal" | "large" | "x-large" variant?: "default" | "settings" class?: ComponentProps<"div">["class"] + containerClass?: ComponentProps<"div">["class"] classList?: ComponentProps<"div">["classList"] fit?: boolean } @@ -79,7 +80,7 @@ export function DialogHeader(props: DialogHeaderProps) { } export function Dialog(props: DialogProps) { - const [local] = splitProps(props, ["size", "variant", "class", "classList", "fit", "children"]) + const [local] = splitProps(props, ["size", "variant", "class", "containerClass", "classList", "fit", "children"]) return ( <div @@ -88,7 +89,7 @@ export function Dialog(props: DialogProps) { data-fit={local.fit ? true : undefined} data-size={local.size || "normal"} > - <div data-slot="dialog-container"> + <div data-slot="dialog-container" class={local.containerClass}> <Kobalte.Content data-slot="dialog-content" classList={{ diff --git a/packages/ui/src/v2/components/icon.tsx b/packages/ui/src/v2/components/icon.tsx index d9bd04e48f..b0b70bf37f 100644 --- a/packages/ui/src/v2/components/icon.tsx +++ b/packages/ui/src/v2/components/icon.tsx @@ -133,6 +133,10 @@ const icons = { viewBox: "0 0 20 20", body: `<path d="M5.83333 4.16406L2.5 7.4974L5.83333 10.8307M3.33333 7.4974H17.9167V15.4141H10" stroke="currentColor" stroke-linecap="square"/>`, }, + "outline-reset": { + viewBox: "0 0 20 20", + body: `<path d="M5.83333 4.16406L2.5 7.4974L5.83333 10.8307M3.33333 7.4974H17.9167V15.4141H10" stroke="currentColor" stroke-linecap="square"/>`, + }, archive: { viewBox: "0 0 16 16", body: `<path d="M13.1112 13.5555V14.0555H13.6112V13.5555H13.1112ZM2.889 13.5555H2.389L2.389 14.0555H2.889V13.5555ZM3.38901 5.55546L3.38901 5.05546L2.38901 5.05546L2.38901 5.55546L2.88901 5.55546L3.38901 5.55546ZM14.4446 2.44434H14.9446V1.94434L14.4446 1.94434L14.4446 2.44434ZM14.4446 5.55545L14.4446 6.05545L14.9446 6.05545V5.55545H14.4446ZM1.55566 5.55546L1.05566 5.55545L1.05566 6.05546L1.55566 6.05546L1.55566 5.55546ZM1.5557 2.44436L1.5557 1.94436L1.05571 1.94436L1.0557 2.44435L1.5557 2.44436ZM13.1112 5.55546H12.6112V13.5555H13.1112H13.6112V5.55546H13.1112ZM2.889 13.5555H3.389L3.38901 5.55546L2.88901 5.55546L2.38901 5.55546L2.389 13.5555H2.889ZM14.4446 2.44434H13.9446V5.55545H14.4446H14.9446V2.44434H14.4446ZM1.55566 5.55546L2.05566 5.55547L2.0557 2.44436L1.5557 2.44436L1.0557 2.44435L1.05566 5.55545L1.55566 5.55546ZM6.22234 8.22213V8.72213H9.7779V8.22213V7.72213H6.22234V8.22213ZM13.1112 13.5555V13.0555H2.889V13.5555V14.0555H13.1112V13.5555ZM1.5557 2.44436L1.5557 2.94436L14.4446 2.94434L14.4446 2.44434L14.4446 1.94434L1.5557 1.94436L1.5557 2.44436ZM14.4446 5.55545L14.4446 5.05545L1.55566 5.05546L1.55566 5.55546L1.55566 6.05546L14.4446 6.05545L14.4446 5.55545Z" fill="currentColor"/>`, diff --git a/packages/ui/src/v2/styles/theme.css b/packages/ui/src/v2/styles/theme.css index 5ed135b7e9..8685f4f64b 100644 --- a/packages/ui/src/v2/styles/theme.css +++ b/packages/ui/src/v2/styles/theme.css @@ -77,15 +77,17 @@ --v2-state-fg-info: var(--v2-blue-800); --v2-state-border-info: var(--v2-blue-300); - --v2-agent-plan-solid: var(--v2-pink-900); - --v2-agent-plan-border: rgba(200, 61, 139, 0.5); - --v2-agent-plan-background: rgba(253, 236, 243, 0.33); - --v2-agent-build-solid: var(--v2-blue-900); - --v2-agent-build-border: rgba(59, 92, 246, 0.5); - --v2-agent-build-background: rgba(236, 241, 254, 0.33); + --v2-agent-plan-solid: var(--v2-pink-800); + --v2-agent-plan-border: rgba(200, 61, 139, 0.2); + --v2-agent-plan-background: rgba(253, 236, 243, 0.1); + --v2-agent-build-solid: var(--v2-blue-800); + --v2-agent-build-border: rgba(44, 71, 200, 0.1); + --v2-agent-build-background: rgba(236, 241, 254, 0.1); --v2-agent-explore-solid: var(--v2-yellow-900); - --v2-agent-explore-border: rgba(142, 114, 49, 0.5); - --v2-agent-explore-background: rgba(254, 250, 236, 0.33); + --v2-agent-explore-border: rgba(203, 159, 52, 0.2); + --v2-agent-explore-background: rgba(254, 250, 236, 0.1); + --v2-agent-review-solid: var(--v2-green-800); + --v2-agent-writer-solid: var(--v2-purple-700); /* ── Project avatar (fixed; theme-independent) ── */ --v2-avatar-fg: #ffffffff; @@ -316,15 +318,17 @@ --v2-state-fg-info: var(--v2-blue-800); --v2-state-border-info: var(--v2-blue-300); - --v2-agent-plan-solid: var(--v2-pink-900); - --v2-agent-plan-border: rgba(200, 61, 139, 0.5); - --v2-agent-plan-background: rgba(253, 236, 243, 0.33); - --v2-agent-build-solid: var(--v2-blue-900); - --v2-agent-build-border: rgba(59, 92, 246, 0.5); - --v2-agent-build-background: rgba(236, 241, 254, 0.33); + --v2-agent-plan-solid: var(--v2-pink-800); + --v2-agent-plan-border: rgba(200, 61, 139, 0.2); + --v2-agent-plan-background: rgba(253, 236, 243, 0.1); + --v2-agent-build-solid: var(--v2-blue-800); + --v2-agent-build-border: rgba(44, 71, 200, 0.1); + --v2-agent-build-background: rgba(236, 241, 254, 0.1); --v2-agent-explore-solid: var(--v2-yellow-900); - --v2-agent-explore-border: rgba(142, 114, 49, 0.5); - --v2-agent-explore-background: rgba(254, 250, 236, 0.33); + --v2-agent-explore-border: rgba(203, 159, 52, 0.2); + --v2-agent-explore-background: rgba(254, 250, 236, 0.1); + --v2-agent-review-solid: var(--v2-green-800); + --v2-agent-writer-solid: var(--v2-purple-700); --v2-elevation-raised: 0px 2px 4px 0px var(--v2-alpha-dark-4), 0px 1px 2px -1px var(--v2-alpha-dark-8), @@ -437,14 +441,16 @@ --v2-state-border-info: var(--v2-blue-900); --v2-agent-plan-solid: var(--v2-pink-400); - --v2-agent-plan-border: rgba(250, 188, 216, 0.5); - --v2-agent-plan-background: rgba(247, 213, 228, 0.1); - --v2-agent-build-solid: var(--v2-blue-400); - --v2-agent-build-border: rgba(215, 226, 252, 0.5); - --v2-agent-build-background: rgba(215, 226, 252, 0.1); - --v2-agent-explore-solid: var(--v2-yellow-400); - --v2-agent-explore-border: rgba(247, 229, 181, 0.5); - --v2-agent-explore-background: rgba(252, 239, 208, 0.1); + --v2-agent-plan-border: rgba(247, 153, 198, 0.2); + --v2-agent-plan-background: rgba(170, 53, 118, 0.05); + --v2-agent-build-solid: var(--v2-blue-300); + --v2-agent-build-border: rgba(162, 188, 255, 0.2); + --v2-agent-build-background: rgba(38, 63, 169, 0.05); + --v2-agent-explore-solid: var(--v2-yellow-300); + --v2-agent-explore-border: rgba(243, 218, 155, 0.2); + --v2-agent-explore-background: rgba(172, 136, 51, 0.05); + --v2-agent-review-solid: var(--v2-green-300); + --v2-agent-writer-solid: var(--v2-purple-400); --v2-avatar-fg: #ffffffff; --v2-avatar-bg-orange: #723d22ff; diff --git a/packages/web/package.json b/packages/web/package.json index c65c9c12ea..2782f6a397 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.17.16", + "version": "1.17.18", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index afe3e7e3bc..18963bfcaf 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "CoDev Code for VS Code", - "version": "1.17.16", + "version": "1.17.18", "publisher": "sst-dev", "repository": { "type": "git",