Compare commits

..

72 Commits

Author SHA1 Message Date
Kit Langton 0f40b3f053 Merge remote-tracking branch 'origin/v2' into sync-engine-plan 2026-08-19 15:33:47 -04:00
opencode-agent[bot] 173e3b0d48 chore: update nix node_modules hashes 2026-08-19 18:59:43 +00:00
Kit Langton de7d389ed7 refactor(client): replace store adapter with signal of view
The engine's immutable views are now held in one signal per session;
the Data facade keeps its shape, backed by signal reads instead of
store paths. Deletes the identity diff, per-path reconcile, deep
clone into the store, the rendered shadow map, the mutable
StoreSessionView type, and both hand-maintained invariants (new
fields must be diffed; per-index writes cannot shrink). Views alias
engine state directly, which is safe because views are immutable and
consumers receive fresh arrays from the facade. Clones remain only
where values cross into the legacy layer's store.
2026-08-19 14:40:31 -04:00
Kit Langton 96900eb67d fix(client): clear fold fields by deleting keys
Assigning undefined leaves an own key that JSON snapshots cannot
represent, so replay and fromSnapshot disagreed on key presence
(retry, error, finish, time.completed, revert, text state). The Solid
store adapter masked this: reconcile treats undefined as delete. Clear
semantics now delete the key so both hydration paths agree.
2026-08-19 14:40:31 -04:00
ReStranger 1df9092f07 feat(ci): enable nix-hashes ci in v2 branch (#43482)
Signed-off-by: ReStranger <restranger@disroot.org>
2026-08-19 20:37:59 +02:00
Kit Langton d7562b3161 chore: move perf harness and design docs to follow-up
Slims the review surface to runtime code and tests. The bench script,
perf experiment log, and the two root-level design documents are
preserved on the sync-engine-perf-docs branch and will return in a
follow-up PR. Headline numbers stay in the PR description.
2026-08-19 14:06:09 -04:00
Aiden Cline fbf45f2384 fix(core): centralize native provider mapping (#43468) 2026-08-19 12:32:53 -05:00
ReStranger 9292bfbe49 feat(nix): update cli packaging (#43469)
Signed-off-by: ReStranger <restranger@disroot.org>
2026-08-19 11:19:13 -05:00
Shoubhit Dash 5d453752f0 refactor(core): move location watcher config into state (#43464) 2026-08-19 21:19:32 +05:30
Kit Langton 16b77e5b50 fix(tui): soften jump to latest action (#43457) 2026-08-19 11:41:57 -04:00
Dax c1583ddcbb docs: move CPU profiling to dev skill (#43448) 2026-08-19 11:29:06 -04:00
Kit Langton d8debaa449 fix(tui): stabilize plugin reload generations (#43447) 2026-08-19 15:26:54 +00:00
Shoubhit Dash ace822308f refactor(core): move compaction config into state (#43442) 2026-08-19 20:43:01 +05:30
opencode-agent[bot] 728053b645 fix(tui): avoid retrying broken plugin setup (#43441)
Co-authored-by: nexxeln <95541290+nexxeln@users.noreply.github.com>
2026-08-19 20:41:13 +05:30
opencode-agent[bot] bfe9917ee7 fix(core): lower AI SDK system updates (#43440)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-08-19 10:03:03 -05:00
opencode-agent[bot] 1556b74082 fix(core): preserve Vertex billing labels (#43449)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-08-19 09:57:47 -05:00
Dax 7700faad81 feat(cli): capture CPU profiles with SIGPROF (#43446) 2026-08-19 10:45:05 -04:00
Kit Langton 241a88a5d9 fix(tui): open subagent panel from footer (#43325) 2026-08-19 10:33:44 -04:00
Major Hayden 3edcb3ca2b fix(core): route Vertex Gemini through native provider (#43433)
Signed-off-by: Major Hayden <major@mhtx.net>
2026-08-19 08:59:11 -05:00
opencode-agent[bot] dcc7de2e47 test(app): stabilize reconnect offset timing (#43438)
Co-authored-by: nexxeln <95541290+nexxeln@users.noreply.github.com>
2026-08-19 19:27:34 +05:30
Shoubhit Dash c40c306170 refactor(core): move shell config into state (#43430) 2026-08-19 19:23:22 +05:30
Shoubhit Dash a207253242 fix(core): await snapshot state readiness (#43435) 2026-08-19 19:23:10 +05:30
Shoubhit Dash 1e867c228a refactor(core): move snapshot config into state (#43425) 2026-08-19 18:34:54 +05:30
Shoubhit Dash 8a402d3f03 refactor(core): move tool output config into state (#43422) 2026-08-19 18:25:22 +05:30
opencode-agent[bot] 33567c5792 fix(desktop): connect wildcard service through loopback (#43171)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-19 14:39:54 +10:00
Aiden Cline daf3f9ed08 feat(ai): support Responses tool controls (#43329) 2026-08-18 23:33:04 -05:00
opencode-agent[bot] d5bf8799c0 fix(cli): keep run event stream alive (#43348)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-08-18 23:31:26 -05:00
Dax f6f64d7ece feat(cli): manage plugin packages (#43283) 2026-08-19 00:06:15 -04:00
Aiden Cline 6baad7fc3e feat(ai): support Responses truncation policy (#43339) 2026-08-18 22:54:37 -05:00
Dax 0762d63b6a fix(core): identify user-initiated web fetches (#43330) 2026-08-18 23:51:52 -04:00
Aiden Cline 4df0591025 feat(core): retain provider finish details (#43332) 2026-08-18 22:40:13 -05:00
Aiden Cline 30cb420900 feat(ai): lower system updates as developer (#43326) 2026-08-18 22:20:42 -05:00
Kit Langton 726a11ba4a test(client): review pass on sync-engine test suite
Two parallel reviews (quality + tests-as-spec) over the engine tests,
treating them as first-class code. Accuracy fixes to catalog claims,
fixture honesty, and typecheck coverage:

- typecheck now covers the sync-engine tests and scripts via
  tsconfig.test.json (old test files with pre-existing errors excluded);
  fixes the type errors this surfaced (fixture stream signal param,
  readonly snapshot arrays, invalid assistant literal)
- FakeSessionServer: file header stating the real-fold modeling claim,
  honors the abort signal, heal() so the sim reset cannot drift from the
  faults record, seq() so tests stop leaning on events.length, threads
  the constructor time into the snapshot, fixes AsyncQueue falsy trap
- laws: file header, law 10 numbering for the refresh-race test, vacuous
  law 2 wait replaced with engine.ready(), law 1/2 story comments,
  robust comment stripping in law 3
- sim: header, chaos distribution + drain-phase comments, heal(),
  ses_-prefixed session IDs everywhere
- divergence catalog: corrected engine-side claims (live reordering is
  ruled out by the ordered log, not the cursor check; activity resync
  wording; law 10 citation), header notes the raw-protocol retry test,
  per-call delta event IDs
- engine-data: header linking it to the laws, SNAPSHOT_RECENT named
  constant, error-DTO source-of-truth comment
2026-08-18 23:03:29 -04:00
Kit Langton d31fbab722 test(client): catalog legacy data layer divergence states 2026-08-18 22:42:38 -04:00
Kit Langton 4fcffdd05e docs: record future directions from architecture retrospective 2026-08-18 22:24:13 -04:00
Kit Langton a060459985 docs: clarify engine vs legacy size comparison 2026-08-18 22:14:44 -04:00
Kit Langton 16a45957fd test(client): cover server outages and restarts in sync engine suite 2026-08-18 22:11:51 -04:00
Kit Langton 90832ffe8d refactor(client): simplify engine render and store diff 2026-08-18 21:58:35 -04:00
Luke Parker 3b8d3ca639 refactor(ui): promote current design system (#43200) 2026-08-19 11:36:11 +10:00
Kit Langton 6cbf607849 refactor(client): clone only changed subtrees on engine publish 2026-08-18 21:29:12 -04:00
Kit Langton 378d46c076 chore(client): add session sync streaming benchmark 2026-08-18 21:29:11 -04:00
Kit Langton e0d4a0374c chore(client): regenerate on rebased protocol 2026-08-18 20:56:08 -04:00
Kit Langton f6f8606155 docs: record cursor honesty contract 2026-08-18 20:44:57 -04:00
Kit Langton ef31823a6f fix(client): recover from lossy log attach via snapshot 2026-08-18 20:44:57 -04:00
Kit Langton 4a42f4a724 fix(core): refuse log cursors without retained replay 2026-08-18 20:44:57 -04:00
Kit Langton ce27f07f15 fix(client): preserve minted prompt IDs 2026-08-18 20:44:57 -04:00
Kit Langton 80587d936f docs: wiring complete, regression dispositions 2026-08-18 20:44:57 -04:00
Kit Langton 09772d89d5 fix(tui): restore data-layer guarantees under engine wiring 2026-08-18 20:44:57 -04:00
Kit Langton 4f07025067 fix(client): restore engine refresh guarantees 2026-08-18 20:44:57 -04:00
Kit Langton f253bdaeef feat(tui): wire engine data layer 2026-08-18 20:44:57 -04:00
Kit Langton 93c24cafa7 feat(client): engine-backed data layer 2026-08-18 20:44:57 -04:00
Kit Langton 0963826120 docs: record lane status 2026-08-18 20:44:56 -04:00
Kit Langton c5df2e5c28 refactor(core): simplify snapshot and log changes 2026-08-18 20:44:56 -04:00
Kit Langton 13f0cdd0ea feat(core): session.log follow includes ephemeral events 2026-08-18 20:44:32 -04:00
Kit Langton 5887911c26 feat(core): typed seq-unavailable on session.log 2026-08-18 20:44:01 -04:00
Kit Langton 59bc653465 feat(protocol): session.snapshot endpoint 2026-08-18 20:44:01 -04:00
Kit Langton 8a4c14aed5 refactor(client): simplify sync engine 2026-08-18 20:43:47 -04:00
Kit Langton 15aef5f0f2 test(client): seeded chaos sim 2026-08-18 20:43:47 -04:00
Kit Langton 9b87120db6 test(client): six laws 2026-08-18 20:43:47 -04:00
Kit Langton af6407a1b1 feat(client): engine core 2026-08-18 20:43:47 -04:00
Kit Langton e4ff762483 feat(client): session fold 2026-08-18 20:43:47 -04:00
Kit Langton a00822b06e feat(core): Session.snapshot 2026-08-18 20:43:47 -04:00
Kit Langton 58c33f6870 docs: deletion inventory at adoption 2026-08-18 20:43:21 -04:00
Kit Langton 82d13c6715 docs: outbox ack contract, feature-grouped commits, opt-in ephemeral stream 2026-08-18 20:43:21 -04:00
Kit Langton 6c1d066a65 docs: drop adopt-strictness (S2), renumber lanes 2026-08-18 20:43:21 -04:00
Kit Langton 9b8801a3c6 docs: commit-by-commit implementation lanes, snapshot recent param 2026-08-18 20:43:21 -04:00
Kit Langton 8dad59cf99 docs: delivery is a parallel build for local comparison, not a staged migration 2026-08-18 20:43:21 -04:00
Kit Langton a023ece71f docs: snapshot-only entry contract, compaction/revert/fork, ambient bus filter (S4) 2026-08-18 20:43:21 -04:00
Kit Langton 2b5cc0090a docs: single session stream, overlay semantics, mid-stream reconnect 2026-08-18 20:43:21 -04:00
Kit Langton fc6b9e0dde docs: fold prior-art findings into snapshot decision 2026-08-18 20:43:20 -04:00
Kit Langton ab535c3875 docs: decide snapshot endpoint shape (tentative) 2026-08-18 20:43:20 -04:00
Kit Langton 4ca2f9dd83 docs: sync engine plan outline 2026-08-18 20:43:20 -04:00
420 changed files with 10014 additions and 11076 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ permissions:
on:
workflow_dispatch:
push:
branches: [dev, beta]
branches: [dev, beta, v2]
paths:
- "bun.lock"
- "package.json"
+27
View File
@@ -185,6 +185,33 @@ pmap -x <pid> | sort -k3 -nr | head -25
Heap serialization itself can temporarily increase RSS and allocator high-water marks, so record `ps`/`smaps_rollup` both before and after capture. Large anonymous mappings with a comparatively small live heap require native-allocation or allocator investigation; they cannot be explained from JavaScript retainer paths alone.
## CPU profiles
The CLI installs a `SIGPROF` listener on non-Windows processes in `packages/cli/src/cpu-profile.ts`. One signal starts a ten-second CPU profile and stops it automatically; additional signals are ignored while a profile is active. There is no CPU profile CLI flag or environment variable.
1. Get the PID from the health endpoint. For shared-service performance, target the server PID returned here rather than the short wrapper or TUI process:
```bash
opencode2 api get /api/health
```
Use `bun dev api get /api/health` instead when targeting the local/dev channel.
2. Start the capture:
```bash
kill -PROF <server-pid>
```
3. Wait for `CPU profile written` in the channel's log before opening the file. Profiles are written to the same log directory as `cpu-<pid>-<timestamp>.cpuprofile`; the log's `path=` field is authoritative:
```bash
grep 'CPU profile' ~/.local/share/opencode/log/opencode.log | tail
find ~/.local/share/opencode/log -maxdepth 1 -name 'cpu-<server-pid>-*.cpuprofile' -printf '%T@ %s %p\n' | sort -nr | head
```
Use `opencode-local.log` for a local/dev process. Load the completed `.cpuprofile` in Chrome DevTools or another V8 CPU profile viewer and inspect the hottest functions, call stacks, and self time during the controlled workload.
## Debugger
- To debug the V2 CLI or TUI with Bun's inspector, launch the CLI entrypoint through Terminal Control with an inspector URL, then attach a debugger to that URL:
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-uduwrM143NDSc+tXsi4lVVfoMll2a3BDHRUjuO7GB68=",
"aarch64-linux": "sha256-6DUda78XdXY6DP86lIUkweSjys3iG4Y4mo1PiaNuXbg=",
"aarch64-darwin": "sha256-AkJwfLULLZVwwz+XU1QcFUZoIS7oVPCn+n/MXEaxrqE=",
"x86_64-darwin": "sha256-hAxKGdiITTxQ2uujQt6prNjo3NxGAMMeo+9HlMWK6GU="
"x86_64-linux": "sha256-IxkSw0gK/qkMHZGVHqjwgM9BKhzbQX6hyF9SWUNtpzg=",
"aarch64-linux": "sha256-YVjpbil0QswVwi6NtVYFq3xCqpsfveG1chlNVCVI0MU=",
"aarch64-darwin": "sha256-CdL2mI84pawH2H5i9qu8A6IWbkmKOYHlJS+DI/Mafdw=",
"x86_64-darwin": "sha256-NtswwfU5WYv99bEmI4XeLwjhBGcS9ZMYLRo4MQRNtLo="
}
}
+2 -2
View File
@@ -10,7 +10,7 @@
]).nodeModules.${stdenvNoCC.hostPlatform.system},
}:
let
packageJson = lib.pipe ../packages/opencode/package.json [
packageJson = lib.pipe ../packages/cli/package.json [
builtins.readFile
builtins.fromJSON
];
@@ -52,7 +52,7 @@ stdenvNoCC.mkDerivation {
--cpu="${bunCpu}" \
--os="${bunOs}" \
--filter '!./' \
--filter './packages/opencode' \
--filter './packages/cli' \
--filter './packages/desktop' \
--filter './packages/app' \
--frozen-lockfile \
+8 -10
View File
@@ -48,13 +48,13 @@ stdenvNoCC.mkDerivation (finalAttrs: {
env.OPENCODE_DISABLE_MODELS_FETCH = true;
env.OPENCODE_VERSION = finalAttrs.version;
env.OPENCODE_CHANNEL = "prod";
env.NODE_OPTIONS = "--max-old-space-size=4096";
buildPhase = ''
runHook preBuild
cd ./packages/opencode
cd ./packages/cli
bun --bun ./script/build.ts --single --skip-install
bun --bun ./script/schema.ts schema.json
runHook postBuild
'';
@@ -62,10 +62,9 @@ stdenvNoCC.mkDerivation (finalAttrs: {
installPhase = ''
runHook preInstall
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
install -Dm644 schema.json $out/share/opencode/schema.json
install -Dm755 dist/cli-*/bin/opencode2 $out/bin/opencode2
wrapProgram $out/bin/opencode \
wrapProgram $out/bin/opencode2 \
--prefix PATH : ${
lib.makeBinPath (
[
@@ -81,9 +80,9 @@ stdenvNoCC.mkDerivation (finalAttrs: {
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
# trick yargs into also generating zsh completions
installShellCompletion --cmd opencode \
--bash <($out/bin/opencode completion) \
--zsh <(SHELL=/bin/zsh $out/bin/opencode completion)
installShellCompletion --cmd opencode2 \
--bash <($out/bin/opencode2 completion) \
--zsh <(SHELL=/bin/zsh $out/bin/opencode2 completion)
'';
nativeInstallCheckInputs = [
@@ -95,7 +94,6 @@ stdenvNoCC.mkDerivation (finalAttrs: {
versionCheckProgramArg = "--version";
passthru = {
jsonschema = "${placeholder "out"}/share/opencode/schema.json";
env = finalAttrs.env;
};
@@ -103,7 +101,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
description = "The open source coding agent";
homepage = "https://opencode.ai";
license = lib.licenses.mit;
mainProgram = "opencode";
mainProgram = "opencode2";
inherit (node_modules.meta) platforms;
};
})
+1 -1
View File
@@ -193,7 +193,7 @@ If you find yourself copying a 3-to-5-line snippet between two protocols, lift i
`LLMRequest.system` is the initial privileged prompt that applies ahead of the conversation. `Message.system(...)` is a separate, provider-neutral chronological operator update inside `LLMRequest.messages`; it applies only from its position in history onward and accepts text content only.
Native chronological system messages are route/model-specific. Anthropic Messages lowers them natively for Claude Opus 4.8 (`claude-opus-4-8`). Other routes and models intentionally lower the update in place into ordinary user-compatible text using this stable escaped representation:
Native chronological system messages are route/model-specific. Open Responses lowers them to standard `developer` messages, while Anthropic Messages lowers them to native system messages for Claude Opus 4.8 (`claude-opus-4-8`). Other routes and models intentionally lower the update in place into ordinary user-compatible text using this stable escaped representation:
```text
<system-update>
+29 -9
View File
@@ -90,6 +90,7 @@ const OpenResponsesFunctionCallOutput = Schema.Union([
export const InputItem = Schema.Union([
Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
Schema.Struct({ role: Schema.tag("developer"), content: Schema.String }),
Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenResponsesInputContent) }),
Schema.Struct({
role: Schema.tag("assistant"),
@@ -140,6 +141,11 @@ export const Tool = Schema.Struct({
export const ToolChoice = Schema.Union([
Schema.Literals(["auto", "none", "required"]),
Schema.Struct({ type: Schema.tag("function"), name: Schema.String }),
Schema.Struct({
type: Schema.tag("allowed_tools"),
mode: Schema.Literals(["auto", "none", "required"]),
tools: Schema.Array(Schema.Struct({ type: Schema.tag("function"), name: Schema.String })),
}),
])
// Fields shared between the HTTP body and the WebSocket `response.create`
@@ -153,6 +159,7 @@ export const coreFields = {
tools: optionalArray(Tool),
tool_choice: Schema.optional(ToolChoice),
store: Schema.optional(Schema.Boolean),
truncation: Schema.optional(OpenResponsesOptions.TruncationSchema),
service_tier: Schema.optional(OpenResponsesOptions.ServiceTierSchema),
prompt_cache_key: Schema.optional(Schema.String),
include: optionalArray(OpenResponsesOptions.ResponseIncludableSchema),
@@ -168,6 +175,8 @@ export const coreFields = {
}),
),
max_output_tokens: Schema.optional(Schema.Number),
max_tool_calls: Schema.optional(Schema.Int),
parallel_tool_calls: Schema.optional(Schema.Boolean),
temperature: Schema.optional(Schema.Number),
top_p: Schema.optional(Schema.Number),
}
@@ -439,14 +448,10 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
for (const message of request.messages) {
if (message.role === "system") {
const part = yield* ProviderShared.wrappedSystemUpdate(extension.name, message)
const previous = input.at(-1)
if (previous && "role" in previous && previous.role === "user")
input[input.length - 1] = {
role: "user",
content: [...previous.content, { type: "input_text", text: part.text }],
}
else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] })
input.push({
role: "developer",
content: ProviderShared.joinText(yield* ProviderShared.systemUpdateText(extension.name, message)),
})
continue
}
@@ -580,6 +585,19 @@ const lowerOptions = (request: LLMRequest) => {
: {}),
...(options.textVerbosity ? { text: { verbosity: options.textVerbosity } } : {}),
...(options.serviceTier ? { service_tier: options.serviceTier } : {}),
...(options.maxToolCalls !== undefined ? { max_tool_calls: options.maxToolCalls } : {}),
...(options.parallelToolCalls !== undefined ? { parallel_tool_calls: options.parallelToolCalls } : {}),
...(options.truncation ? { truncation: options.truncation } : {}),
}
}
const allowedToolChoice = (request: LLMRequest) => {
const allowed = OpenResponsesOptions.resolve(request).allowedTools
if (!allowed) return undefined
return {
type: "allowed_tools" as const,
mode: allowed.mode,
tools: allowed.toolNames.map((name) => ({ type: "function" as const, name })),
}
}
@@ -602,7 +620,9 @@ export const fromRequestWithExtension = Effect.fn("OpenResponses.fromRequestWith
ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility),
),
),
tool_choice: request.toolChoice ? yield* lowerToolChoice(extension.name, request.toolChoice) : undefined,
tool_choice:
allowedToolChoice(request) ??
(request.toolChoice ? yield* lowerToolChoice(extension.name, request.toolChoice) : undefined),
stream: true as const,
max_output_tokens: generation?.maxTokens,
temperature: generation?.temperature,
@@ -121,7 +121,8 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request:
: yield* Effect.forEach(request.tools, (tool) =>
lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility)),
),
tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice, request.tools) : undefined,
tool_choice:
body.tool_choice ?? (request.toolChoice ? yield* lowerToolChoice(request.toolChoice, request.tools) : undefined),
} satisfies OpenAIResponsesBody
})
@@ -1,4 +1,4 @@
import { Schema } from "effect"
import { Option, Schema } from "effect"
import { TextVerbosity, type LLMRequest } from "../../schema/index.js"
export const ResponseIncludables = [
@@ -11,52 +11,62 @@ export const ResponseIncludables = [
"reasoning.encrypted_content",
"message.output_text.logprobs",
] as const
export type ResponseIncludable = (typeof ResponseIncludables)[number]
export type ResponseIncludable = (typeof ResponseIncludables)[number] | (string & {})
export const ServiceTiers = ["auto", "default", "flex", "priority"] as const
export type ServiceTier = (typeof ServiceTiers)[number]
const TEXT_VERBOSITY = new Set<string>(["low", "medium", "high"])
const INCLUDABLES = new Set<string>(ResponseIncludables)
const SERVICE_TIERS = new Set<string>(ServiceTiers)
const isTextVerbosity = (value: unknown): value is Schema.Schema.Type<typeof TextVerbosity> =>
typeof value === "string" && TEXT_VERBOSITY.has(value)
const isServiceTier = (value: unknown): value is ServiceTier => typeof value === "string" && SERVICE_TIERS.has(value)
export const Truncations = ["auto", "disabled"] as const
export type Truncation = (typeof Truncations)[number]
export const ReasoningEffort = Schema.String
export const TextVerbositySchema = TextVerbosity
export const ResponseIncludableSchema = Schema.Literals(ResponseIncludables)
export const ResponseIncludableSchema = Schema.declare<ResponseIncludable>(
(value): value is ResponseIncludable => typeof value === "string",
{ title: "ResponseIncludable" },
)
export const ServiceTierSchema = Schema.Literals(ServiceTiers)
export const TruncationSchema = Schema.Literals(Truncations)
export interface Resolved {
readonly instructions?: string
readonly store?: boolean
readonly reasoningEffort?: string
readonly reasoningSummary?: "auto" | "concise" | "detailed"
readonly include?: ReadonlyArray<ResponseIncludable>
readonly textVerbosity?: Schema.Schema.Type<typeof TextVerbosity>
readonly serviceTier?: ServiceTier
export const AllowedTools = Schema.Struct({
toolNames: Schema.Array(Schema.String),
mode: Schema.optional(Schema.Literals(["auto", "none", "required"])),
})
export type AllowedTools = typeof AllowedTools.Type
export const Options = Schema.Struct({
instructions: Schema.optional(Schema.String),
store: Schema.optional(Schema.Boolean),
reasoningEffort: Schema.optional(ReasoningEffort),
reasoningSummary: Schema.optional(Schema.Literals(["auto", "concise", "detailed"])),
include: Schema.optional(Schema.Array(ResponseIncludableSchema)),
textVerbosity: Schema.optional(TextVerbositySchema),
serviceTier: Schema.optional(ServiceTierSchema),
truncation: Schema.optional(TruncationSchema),
allowedTools: Schema.optional(AllowedTools),
maxToolCalls: Schema.optional(Schema.Int),
parallelToolCalls: Schema.optional(Schema.Boolean),
})
export type Options = typeof Options.Type
export type Resolved = Omit<Options, "allowedTools"> & {
readonly allowedTools?: AllowedTools & { readonly mode: NonNullable<AllowedTools["mode"]> }
}
const decodeOptions = Schema.decodeUnknownOption(Options)
export const resolve = (request: LLMRequest): Resolved => {
const input = request.providerOptions?.[request.model.route.providerMetadataKey ?? "openresponses"]
const include = Array.isArray(input?.include)
? input.include.filter((entry): entry is ResponseIncludable => INCLUDABLES.has(entry))
: []
const reasoningSummary = input?.reasoningSummary
const input = Option.getOrUndefined(
decodeOptions(request.providerOptions?.[request.model.route.providerMetadataKey ?? "openresponses"]),
)
if (!input) return {}
return {
instructions: typeof input?.instructions === "string" ? input.instructions : undefined,
store: typeof input?.store === "boolean" ? input.store : undefined,
reasoningEffort: typeof input?.reasoningEffort === "string" ? input.reasoningEffort : undefined,
reasoningSummary:
reasoningSummary === "auto" || reasoningSummary === "concise" || reasoningSummary === "detailed"
? reasoningSummary
...input,
include: input.include?.length ? input.include : undefined,
allowedTools:
input.allowedTools && input.allowedTools.toolNames.length > 0
? { ...input.allowedTools, mode: input.allowedTools.mode ?? "auto" }
: undefined,
include: include.length > 0 ? include : undefined,
textVerbosity: isTextVerbosity(input?.textVerbosity) ? input.textVerbosity : undefined,
serviceTier: isServiceTier(input?.serviceTier) ? input.serviceTier : undefined,
}
}
@@ -1,16 +1,7 @@
import type { ResponseIncludable, ServiceTier } from "../protocols/utils/open-responses-options.js"
import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema/index.js"
import type { Options } from "../protocols/utils/open-responses-options.js"
import type { ProviderOptions } from "../schema/index.js"
export interface OpenResponsesOptionsInput {
readonly [key: string]: unknown
readonly instructions?: string
readonly store?: boolean
readonly reasoningEffort?: ReasoningEffort
readonly reasoningSummary?: "auto" | "concise" | "detailed"
readonly include?: ReadonlyArray<ResponseIncludable>
readonly textVerbosity?: TextVerbosity
readonly serviceTier?: ServiceTier
}
export type OpenResponsesOptionsInput = Options & { readonly [key: string]: unknown }
export type OpenResponsesProviderOptionsInput = ProviderOptions & {
readonly openresponses?: OpenResponsesOptionsInput
@@ -19,6 +19,7 @@ export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL: string
readonly provider?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type FamilyModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
@@ -75,6 +76,7 @@ export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsIn
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
limits: settings.limits,
provider: settings.provider,
providerOptions: settings.providerOptions,
}).model(modelID)
export const baseten = define(profiles.baseten)
+13 -3
View File
@@ -7,6 +7,7 @@ import {
type FinishReasonDetails,
type AIError,
type LLMRequest,
type ProviderMetadata,
type UsageInput,
} from "./schema/index.js"
import { Context, Deferred, Effect, Latch, Layer, Queue, Scope, Stream } from "effect"
@@ -33,13 +34,22 @@ export interface LayerOptions {
export class Service extends Context.Service<Service, Interface>()("@opencode/ai/TestLLM") {}
export const complete = (
options: { readonly reason: FinishReasonDetails; readonly usage?: UsageInput },
options: {
readonly reason: FinishReasonDetails
readonly usage?: UsageInput
readonly providerMetadata?: ProviderMetadata
},
...events: readonly LLMEvent[]
) => [
LLMEvent.stepStart({ index: 0 }),
...events,
LLMEvent.stepFinish({ index: 0, reason: options.reason, usage: options.usage }),
LLMEvent.finish({ reason: options.reason }),
LLMEvent.stepFinish({
index: 0,
reason: options.reason,
usage: options.usage,
providerMetadata: options.providerMetadata,
}),
LLMEvent.finish({ reason: options.reason, providerMetadata: options.providerMetadata }),
]
export const stop = (...events: readonly LLMEvent[]) => complete({ reason: { normalized: "stop" } }, ...events)
@@ -1,6 +1,6 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMEvent, Message } from "../../src/index.js"
import { LLM, LLMEvent, Message, ToolDefinition } from "../../src/index.js"
import { configure } from "../../src/providers/openai-compatible-responses.js"
import { OpenAI } from "../../src/providers.js"
import { OpenResponses } from "../../src/protocols/open-responses.js"
@@ -56,6 +56,28 @@ describe("Open Responses-compatible route", () => {
}),
)
it.effect("lowers chronological system updates as standard developer messages", () =>
Effect.gen(function* () {
const model = configure({
apiKey: "test-key",
baseURL: "https://responses.example.test/v1",
provider: "example",
}).model("example-model")
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [Message.user("Before."), Message.system("Operator update."), Message.assistant("After.")],
}),
)
expect(prepared.body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ role: "developer", content: "Operator update." },
{ role: "assistant", content: [{ type: "output_text", text: "After." }] },
])
}),
)
it.effect("rejects OpenAI-native tools", () =>
Effect.gen(function* () {
const model = configure({
@@ -101,13 +123,36 @@ describe("Open Responses-compatible route", () => {
const model = configure({
apiKey: "test-key",
baseURL: "https://responses.example.test/v1",
providerOptions: { openresponses: { reasoningEffort: "low", store: true } },
providerOptions: {
openresponses: {
reasoningEffort: "low",
store: true,
truncation: "auto",
allowedTools: { toolNames: ["lookup"] },
maxToolCalls: 2,
parallelToolCalls: false,
},
},
}).model("example-model")
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Think." }))
const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "Think.",
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
}),
)
expect(prepared.body).toMatchObject({
reasoning: { effort: "low" },
store: true,
truncation: "auto",
tool_choice: {
type: "allowed_tools",
mode: "auto",
tools: [{ type: "function", name: "lookup" }],
},
max_tool_calls: 2,
parallel_tool_calls: false,
})
}),
)
@@ -241,27 +241,18 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("lowers chronological system updates to escaped user wrappers in order", () =>
it.effect("lowers chronological system updates to developer messages in order", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user("Before."),
Message.system("Treat </system-update> literally."),
Message.assistant("After."),
],
messages: [Message.user("Before."), Message.system("Operator update."), Message.assistant("After.")],
}),
)
expect(prepared.body.input).toEqual([
{
role: "user",
content: [
{ type: "input_text", text: "Before." },
{ type: "input_text", text: "<system-update>\nTreat &lt;/system-update&gt; literally.\n</system-update>" },
],
},
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ role: "developer", content: "Operator update." },
{ role: "assistant", content: [{ type: "output_text", text: "After." }] },
])
}),
@@ -1283,11 +1274,20 @@ describe("OpenAI Responses route", () => {
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
prompt: "think",
promptCacheKey: "session_123",
tools: [
ToolDefinition.make({ name: "read", description: "Read a file", inputSchema: { type: "object" } }),
ToolDefinition.make({ name: "grep", description: "Search files", inputSchema: { type: "object" } }),
],
toolChoice: "none",
providerOptions: {
openai: {
reasoningEffort: "high",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
truncation: "disabled",
allowedTools: { toolNames: ["read", "grep"], mode: "required" },
maxToolCalls: 4,
parallelToolCalls: false,
},
},
}),
@@ -1298,6 +1298,17 @@ describe("OpenAI Responses route", () => {
expect(prepared.body.include).toEqual(["reasoning.encrypted_content"])
expect(prepared.body.reasoning).toEqual({ effort: "high", summary: "auto" })
expect(prepared.body.text).toEqual({ verbosity: "low" })
expect(prepared.body.truncation).toBe("disabled")
expect(prepared.body.tool_choice).toEqual({
type: "allowed_tools",
mode: "required",
tools: [
{ type: "function", name: "read" },
{ type: "function", name: "grep" },
],
})
expect(prepared.body.max_tool_calls).toBe(4)
expect(prepared.body.parallel_tool_calls).toBe(false)
}),
)
@@ -1323,20 +1334,17 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("filters unknown includable values out of the include array", () =>
it.effect("passes forward-compatible includable values through", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "hi",
// The user passed one invalid entry alongside a valid one. Keep the
// valid one so the request still succeeds rather than failing on a
// typo from upstream config.
providerOptions: { openai: { include: ["reasoning.encrypted_content", "bogus.thing"] } },
}),
)
expect(prepared.body.include).toEqual(["reasoning.encrypted_content"])
expect(prepared.body.include).toEqual(["reasoning.encrypted_content", "bogus.thing"])
}),
)
@@ -1350,13 +1358,13 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("treats an all-invalid include as no include at all", () =>
it.effect("passes an unknown includable value through", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: ["bogus.thing"] } } }),
)
expect(prepared.body.include).toBeUndefined()
expect(prepared.body.include).toEqual(["bogus.thing"])
}),
)
@@ -25,7 +25,7 @@ async function installSessionSwitchProbe(
let running = true
const reviewLevels: Record<string, string> = {
panel: "#review-panel",
tabs: '#review-panel [data-component="tabs"]',
tabs: '#review-panel [data-component="tabs"]',
body: '#review-panel [data-slot="session-review-v2-body"]',
review: '#review-panel [data-component="session-review-v2"]',
preview: '#review-panel [data-slot="session-review-v2-preview"]',
@@ -52,7 +52,7 @@ test("shows a pending question dock", async ({ page }) => {
rejectRequests.push(request.url())
})
await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click()
await question.getByRole("button", { name: "Minimize question" }).click()
await expect(question).toBeVisible()
await expect(question.getByText("Which implementation should be used?")).toBeVisible()
await expect(question.getByText("Select one answer")).toBeHidden()
@@ -63,7 +63,7 @@ test("shows a pending question dock", async ({ page }) => {
await expect(page.locator('[data-component="question-minimized-dock"]')).toHaveCount(0)
expect(rejectRequests).toEqual([])
await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click()
await question.getByRole("button", { name: "Restore question" }).click()
await expect(question).toBeVisible()
await expect(question.getByText("Which implementation should be used?")).toBeVisible()
await expect(question.getByRole("radio", { name: /Minimal/ })).toBeVisible()
@@ -131,15 +131,14 @@ test("labels V2 skill tools from IDs and result metadata", async ({ page }) => {
"aria-label",
"sample-skill",
)
await expect(page.locator(`[data-timeline-part-id="${completed}"] [data-component="text-shimmer"]`)).toHaveAttribute(
"aria-label",
"OpenCode",
)
await expect(
page.locator(`[data-timeline-part-id="${completed}"] [data-component="text-shimmer"]`),
).toHaveAttribute("aria-label", "OpenCode")
for (const id of [pending, completed]) {
const skill = page.locator(`[data-timeline-part-id="${id}"]`)
await expect(skill.locator('[data-slot="skill-tool-label"]')).toHaveText("Skill")
await expect(skill.locator('[data-slot="skill-tool-separator"]')).toHaveText("·")
await expect(skill.locator('use[href="#opencode-icon-post-skill"]')).toBeVisible()
await expect(skill.locator('use[href="#opencode-v2-icon-post-skill"]')).toBeVisible()
}
})
@@ -477,7 +477,7 @@ async function configureSmokePage(page: Page, directory: string) {
}
let recordFrame: number | undefined
const record = () => {
for (const toast of document.querySelectorAll<HTMLElement>('[data-component="toast"][data-variant="error"]')) {
for (const toast of document.querySelectorAll<HTMLElement>(".toast-v2--error")) {
const text = toast.textContent?.trim()
if (text && !smoke.__timelineSmokeErrorToasts!.includes(text)) smoke.__timelineSmokeErrorToasts!.push(text)
}
+2 -19
View File
@@ -3,7 +3,6 @@ import { batch, createEffect, onCleanup, onMount, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
@@ -102,16 +101,8 @@ function Cell(props: {
</div>
)
if (props.inline) {
return (
<TooltipV2 value={props.tip} placement="top">
{content()}
</TooltipV2>
)
}
return (
<Tooltip value={props.tip} placement="top">
<Tooltip appearance={props.inline ? "compact" : "standard"} value={props.tip} placement="top">
{content()}
</Tooltip>
)
@@ -151,16 +142,8 @@ function ToggleCell(props: {
</button>
)
if (props.inline) {
return (
<TooltipV2 value={props.tip} placement="top">
{content()}
</TooltipV2>
)
}
return (
<Tooltip value={props.tip} placement="top">
<Tooltip appearance={props.inline ? "compact" : "standard"} value={props.tip} placement="top">
{content()}
</Tooltip>
)
@@ -1,10 +1,10 @@
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { Dialog, DialogBody } from "@opencode-ai/ui/v2/dialog-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { Dialog, DialogBody } from "@opencode-ai/ui/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { TextInput } from "@opencode-ai/ui/text-input"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js"
import { commandPaletteOptions, formatKeybindParts, useCommand } from "@/context/command"
@@ -191,7 +191,7 @@ function CommandPaletteView(props: {
<Dialog class="command-palette-v2" size="large">
<DialogBody class="command-palette-v2-body">
<div class="command-palette-v2-search">
<TextInputV2
<TextInput
value={query()}
autofocus
autocomplete="off"
@@ -295,7 +295,7 @@ function PaletteRow(props: {
</div>
</div>
<Show when={props.item.keybind}>
<KeybindV2 keys={formatKeybindParts(props.item.keybind ?? "", props.language.t)} variant="neutral" />
<Keybind keys={formatKeybindParts(props.item.keybind ?? "", props.language.t)} variant="neutral" />
</Show>
</Match>
<Match when={props.item.type === "session"}>
@@ -13,7 +13,7 @@ function ConnectProviderDialogStory() {
onMount(open)
return (
<Button variant="secondary" onClick={open}>
<Button variant="neutral" onClick={open}>
Open connect provider dialog
</Button>
)
@@ -29,7 +29,7 @@ function ProviderConnectionDialogStory(props) {
onMount(open)
return (
<Button variant="secondary" onClick={open}>
<Button variant="neutral" onClick={open}>
Open {props.provider} connection dialog
</Button>
)
@@ -5,9 +5,8 @@ import { List } from "@opencode-ai/ui/list"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Spinner } from "@opencode-ai/ui/spinner"
import { TextField } from "@opencode-ai/ui/text-field"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { DialogBody, DialogHeader, DialogTitle, Dialog } from "@opencode-ai/ui/dialog"
import { TextInput } from "@opencode-ai/ui/text-input"
import { showToast } from "@/utils/toast"
import { type Component, createMemo, createUniqueId, For, Match, onMount, Show, Switch } from "solid-js"
import { createStore } from "solid-js/store"
@@ -75,7 +74,7 @@ export const DialogConnectProvider: Component<{
}
return (
<DialogV2
<Dialog
containerClass="!h-[min(calc(100vh_-_16px),512px)] !w-[min(calc(100vw_-_16px),640px)]"
class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]"
>
@@ -99,7 +98,7 @@ export const DialogConnectProvider: Component<{
<Content />
</div>
</DialogBody>
</DialogV2>
</Dialog>
)
}
@@ -168,7 +167,7 @@ function ProviderPicker(props: { directory?: string; onSelect: (provider: string
return (
<div ref={picker} class="flex min-h-0 flex-1 flex-col gap-4" onKeyDown={handleKeyDown}>
<div class="shrink-0 px-1 pt-px">
<TextInputV2
<TextInput
ref={search}
type="search"
class="!w-full [font-family:var(--v2-font-family-sans)]"
@@ -379,7 +378,7 @@ function ProviderConnection(props: {
setFormStore("value", field.key, value)
}}
/>
<Button class="w-auto" type="submit" size="large" variant="primary" disabled={!valid()}>
<Button class="w-auto" type="submit" size="large" variant="contrast" disabled={!valid()}>
{language.t("common.continue")}
</Button>
</Match>
@@ -516,7 +515,7 @@ function ProviderConnection(props: {
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-5 self-stretch">
<label class="flex w-full flex-col gap-1 font-[530] leading-4 text-v2-text-text-base">
{language.t("provider.connect.apiKey.label", { provider: provider().name })}
<TextInputV2
<TextInput
ref={apiKey}
class="!w-full"
name="apiKey"
@@ -537,9 +536,9 @@ function ProviderConnection(props: {
</div>
)}
</Show>
<ButtonV2 type="submit" variant="contrast" data-action="provider-connect-submit">
<Button type="submit" variant="contrast" data-action="provider-connect-submit">
{language.t("common.continue")}
</ButtonV2>
</Button>
</form>
</div>
)
@@ -585,7 +584,7 @@ function ProviderConnection(props: {
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-5 self-stretch">
<label class="flex w-full flex-col gap-1 font-[530] leading-4 text-v2-text-text-base">
{language.t("provider.connect.oauth.code.label", { method: controller.currentMethod()?.label ?? "" })}
<TextInputV2
<TextInput
ref={codeInput}
class="!w-full"
name="code"
@@ -605,9 +604,9 @@ function ProviderConnection(props: {
</div>
)}
</Show>
<ButtonV2 type="submit" variant="contrast">
<Button type="submit" variant="contrast">
{language.t("common.continue")}
</ButtonV2>
</Button>
</form>
</div>
)
@@ -1,6 +1,7 @@
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { useMutation } from "@tanstack/solid-query"
@@ -21,20 +22,21 @@ export function DialogCustomProvider(props: Props) {
const language = useLanguage()
return (
<Dialog
class="h-full"
title={
<IconButton
tabIndex={-1}
icon="arrow-left"
variant="ghost"
onClick={props.onBack}
aria-label={language.t("common.goBack")}
/>
}
transition
>
<CustomProviderForm />
<Dialog class="h-full">
<DialogHeader>
<DialogTitle>
<IconButton
tabIndex={-1}
icon={<Icon name="arrow-left" />}
variant="ghost"
onClick={props.onBack}
aria-label={language.t("common.goBack")}
/>
</DialogTitle>
</DialogHeader>
<DialogBody>
<CustomProviderForm />
</DialogBody>
</Dialog>
)
}
@@ -238,7 +240,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
</div>
<IconButton
type="button"
icon="trash"
icon={<Icon name="trash" />}
variant="ghost"
class="mt-1.5"
onClick={() => removeModel(i())}
@@ -282,7 +284,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
</div>
<IconButton
type="button"
icon="trash"
icon={<Icon name="trash" />}
variant="ghost"
class="mt-1.5"
onClick={() => removeHeader(i())}
@@ -301,7 +303,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
class="w-auto self-start"
type="submit"
size="large"
variant="primary"
variant="contrast"
disabled={saveMutation.isPending}
>
{saveMutation.isPending ? language.t("common.saving") : language.t("common.submit")}
@@ -1,12 +1,11 @@
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Dialog, DialogFooter } from "@opencode-ai/ui/v2/dialog-v2"
import { Field } from "@opencode-ai/ui/v2/field-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { Button } from "@opencode-ai/ui/button"
import { Dialog, DialogFooter } from "@opencode-ai/ui/dialog"
import { Field } from "@opencode-ai/ui/field"
import { Icon } from "@opencode-ai/ui/icon"
import { ProjectAvatar, PROJECT_AVATAR_VARIANTS } from "@opencode-ai/ui/v2/project-avatar-v2"
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
import { TextareaV2 } from "@opencode-ai/ui/v2/textarea-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { ProjectAvatar, PROJECT_AVATAR_VARIANTS } from "@opencode-ai/ui/project-avatar"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Textarea } from "@opencode-ai/ui/textarea"
import { TextInput } from "@opencode-ai/ui/text-input"
import { For, Show, createSignal, startTransition } from "solid-js"
import { useLanguage } from "@/context/language"
import { getProjectAvatarVariant, type LocalProject } from "@/context/layout"
@@ -37,46 +36,46 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
const Footer = () => (
<DialogFooter>
<ButtonV2 type="button" variant="neutral" disabled={model.save.isPending} onClick={model.close}>
<Button type="button" variant="neutral" disabled={model.save.isPending} onClick={model.close}>
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2 type="submit" variant="contrast" disabled={!model.supported || model.save.isPending}>
</Button>
<Button type="submit" variant="contrast" disabled={!model.supported || model.save.isPending}>
{model.save.isPending ? language.t("common.saving") : language.t("common.save")}
</ButtonV2>
</Button>
</DialogFooter>
)
return (
<Dialog size="x-large" variant="settings" class="project-settings-v2-dialog">
<TabsV2
<Tabs
orientation="vertical"
variant="settings"
value={tab()}
onChange={(value) => void startTransition(() => setTab(value))}
class="project-settings-v2"
>
<TabsV2.List>
<Tabs.List>
<div class="project-settings-v2-nav">
<TabsV2.Trigger value="general">
<Tabs.Trigger value="general">
<ProjectAvatar
fallback={projectName()}
variant={getProjectAvatarVariant(props.project.icon?.color)}
class="!size-4 shrink-0"
/>
<span class="truncate">{projectName()}</span>
</TabsV2.Trigger>
<TabsV2.Trigger value="scripts">
</Tabs.Trigger>
<Tabs.Trigger value="scripts">
<Icon name="code" size="small" />
{language.t("project.settings.scripts")}
</TabsV2.Trigger>
<TabsV2.Trigger value="extensions">
</Tabs.Trigger>
<Tabs.Trigger value="extensions">
<Icon name="extensions" size="small" />
{language.t("settings.tab.extensions")}
</TabsV2.Trigger>
</Tabs.Trigger>
</div>
</TabsV2.List>
</Tabs.List>
<TabsV2.Content value="general" class="project-settings-v2-panel">
<Tabs.Content value="general" class="project-settings-v2-panel">
<form onSubmit={model.submit} class="project-settings-v2-form">
<div class="project-settings-v2-scroll">
<div class="project-settings-page-header">
@@ -86,7 +85,7 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
<Field>
<Field.Label>{language.t("dialog.project.edit.name")}</Field.Label>
<TextInputV2
<TextInput
autofocus
appearance="large"
class="!w-full"
@@ -132,7 +131,7 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
"opacity-0": !model.store.iconHover,
}}
>
<IconV2 name={model.store.iconOverride ? "close" : "share"} />
<Icon name={model.store.iconOverride ? "close" : "share"} />
</span>
</button>
<input
@@ -188,9 +187,9 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
</div>
<Footer />
</form>
</TabsV2.Content>
</Tabs.Content>
<TabsV2.Content value="scripts" class="project-settings-v2-panel">
<Tabs.Content value="scripts" class="project-settings-v2-panel">
<form onSubmit={model.submit} class="project-settings-v2-form">
<div class="project-settings-v2-scroll">
<div class="project-settings-page-header">
@@ -200,7 +199,7 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
<Field>
<Field.Label>{language.t("dialog.project.edit.worktree.startup")}</Field.Label>
<Field.Prefix>{language.t("dialog.project.edit.worktree.startup.description")}</Field.Prefix>
<TextareaV2
<Textarea
class="!w-full [&_[data-slot=textarea-v2-textarea]]:font-mono"
rows={5}
value={model.store.startup}
@@ -212,12 +211,12 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
</div>
<Footer />
</form>
</TabsV2.Content>
</Tabs.Content>
<TabsV2.Content value="extensions" class="project-settings-v2-panel">
<Tabs.Content value="extensions" class="project-settings-v2-panel">
<ProjectSettingsExtensions />
</TabsV2.Content>
</TabsV2>
</Tabs.Content>
</Tabs>
</Dialog>
)
}
+23 -18
View File
@@ -3,7 +3,7 @@ import { useNavigate, useParams } from "@solidjs/router"
import { useData } from "@/context/server"
import { usePrompt } from "@/context/prompt"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { showToast } from "@/utils/toast"
import { useLanguage } from "@/context/language"
@@ -94,23 +94,28 @@ export const DialogFork: Component = () => {
}
return (
<Dialog title={language.t("command.session.fork")}>
<List
class="flex-1 px-3 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.fork.empty")}
key={(x) => x.id}
items={messages}
filterKeys={["text"]}
onSelect={handleSelect}
>
{(item) => (
<div class="w-full flex items-center gap-2">
<span class="truncate flex-1 min-w-0 text-left font-normal">{item.text}</span>
<span class="text-text-weak shrink-0 font-normal">{item.time}</span>
</div>
)}
</List>
<Dialog>
<DialogHeader>
<DialogTitle>{language.t("command.session.fork")}</DialogTitle>
</DialogHeader>
<DialogBody>
<List
class="flex-1 px-3 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.fork.empty")}
key={(x) => x.id}
items={messages}
filterKeys={["text"]}
onSelect={handleSelect}
>
{(item) => (
<div class="w-full flex items-center gap-2">
<span class="truncate flex-1 min-w-0 text-left font-normal">{item.text}</span>
<span class="text-text-weak shrink-0 font-normal">{item.time}</span>
</div>
)}
</List>
</DialogBody>
</Dialog>
)
}
@@ -1,15 +1,12 @@
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Switch } from "@opencode-ai/ui/switch"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Button } from "@opencode-ai/ui/button"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Dialog as DialogV2, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/v2/dialog-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { Switch as SwitchV2 } from "@opencode-ai/ui/v2/switch-v2"
import { Dialog, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { List } from "@opencode-ai/ui/list"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Switch } from "@opencode-ai/ui/switch"
import { TextInput } from "@opencode-ai/ui/text-input"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { For, Show, type Component } from "solid-js"
import { useLocal } from "@/context/local"
@@ -44,74 +41,80 @@ export const DialogManageModels: Component = () => {
}
return (
<Dialog
title={language.t("dialog.model.manage")}
description={language.t("dialog.model.manage.description")}
action={
<Dialog>
<DialogHeader hideClose>
<DialogTitleGroup
title={language.t("dialog.model.manage")}
description={language.t("dialog.model.manage.description")}
/>
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={handleConnectProvider}>
{language.t("command.provider.connect")}
</Button>
}
>
<List
class="px-3"
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.model.empty")}
key={(x) => `${x?.provider?.id}:${x?.id}`}
items={local.model.list()}
filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
groupBy={(x) => x.provider.id}
groupHeader={(group) => {
const provider = group.items[0].provider
return (
<>
<span>{provider.name}</span>
<Tooltip
placement="top"
value={language.t("dialog.model.manage.provider.toggle", { provider: provider.name })}
>
<Switch
class="-mr-1"
checked={providerVisible(provider.id)}
onChange={(checked) => setProviderVisibility(provider.id, checked)}
hideLabel
</DialogHeader>
<DialogBody>
<List
class="px-3"
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.model.empty")}
key={(x) => `${x?.provider?.id}:${x?.id}`}
items={local.model.list()}
filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
groupBy={(x) => x.provider.id}
groupHeader={(group) => {
const provider = group.items[0].provider
return (
<>
<span>{provider.name}</span>
<Tooltip
appearance="standard"
placement="top"
value={language.t("dialog.model.manage.provider.toggle", { provider: provider.name })}
>
{provider.name}
</Switch>
</Tooltip>
</>
)
}}
sortGroupsBy={(a, b) => {
const aRank = providerRank(a.items[0].provider.id)
const bRank = providerRank(b.items[0].provider.id)
const aPopular = aRank >= 0
const bPopular = bRank >= 0
if (aPopular && !bPopular) return -1
if (!aPopular && bPopular) return 1
return aRank - bRank
}}
onSelect={(x) => {
if (!x) return
const key = { modelID: x.id, providerID: x.provider.id }
local.model.setVisibility(key, !local.model.visible(key))
}}
>
{(i) => (
<div class="w-full flex items-center justify-between gap-x-3">
<span>{i.name}</span>
<div onClick={(e) => e.stopPropagation()}>
<Switch
checked={!!local.model.visible({ modelID: i.id, providerID: i.provider.id })}
onChange={(checked) => {
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
}}
/>
<Switch
appearance="standard"
class="-mr-1"
checked={providerVisible(provider.id)}
onChange={(checked) => setProviderVisibility(provider.id, checked)}
hideLabel
>
{provider.name}
</Switch>
</Tooltip>
</>
)
}}
sortGroupsBy={(a, b) => {
const aRank = providerRank(a.items[0].provider.id)
const bRank = providerRank(b.items[0].provider.id)
const aPopular = aRank >= 0
const bPopular = bRank >= 0
if (aPopular && !bPopular) return -1
if (!aPopular && bPopular) return 1
return aRank - bRank
}}
onSelect={(x) => {
if (!x) return
const key = { modelID: x.id, providerID: x.provider.id }
local.model.setVisibility(key, !local.model.visible(key))
}}
>
{(i) => (
<div class="w-full flex items-center justify-between gap-x-3">
<span>{i.name}</span>
<div onClick={(e) => e.stopPropagation()}>
<Switch
appearance="standard"
checked={!!local.model.visible({ modelID: i.id, providerID: i.provider.id })}
onChange={(checked) => {
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
}}
/>
</div>
</div>
</div>
)}
</List>
)}
</List>
</DialogBody>
</Dialog>
)
}
@@ -154,20 +157,20 @@ export const DialogManageModelsV2: Component = () => {
})
return (
<DialogV2 size="large" variant="settings" class="settings-v2-manage-models-dialog">
<Dialog size="large" variant="settings" class="settings-v2-manage-models-dialog">
<DialogHeader hideClose={true} closeLabel={language.t("common.close")}>
<DialogTitleGroup
title={language.t("dialog.model.manage")}
description={language.t("dialog.model.manage.description")}
/>
<ButtonV2 variant="neutral" icon="plus" onClick={handleConnectProvider}>
<Button variant="neutral" icon="plus" onClick={handleConnectProvider}>
{language.t("command.provider.connect")}
</ButtonV2>
</Button>
</DialogHeader>
<DialogBody class="flex min-h-0 flex-1 flex-col">
<div class="px-4 pt-px pb-3">
<div class="relative">
<TextInputV2
<TextInput
type="search"
appearance="base"
class="!w-full self-stretch"
@@ -182,12 +185,12 @@ export const DialogManageModelsV2: Component = () => {
aria-label={language.t("dialog.model.search.placeholder")}
/>
<Show when={list.filter()}>
<IconButtonV2
<IconButton
type="button"
variant="ghost-muted"
size="small"
class="settings-v2-tab-search-clear"
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
icon={<Icon name="close" size="large" class="text-v2-icon-icon-muted" />}
onClick={() => list.clear()}
aria-label={language.t("common.clear")}
/>
@@ -225,14 +228,14 @@ export const DialogManageModelsV2: Component = () => {
<h3 class="settings-v2-section-title">{group.items[0].provider.name}</h3>
</div>
<div>
<SwitchV2
<Switch
class="mr-6"
checked={providerVisible(group.category)}
onChange={(checked) => setProviderVisibility(group.category, checked)}
hideLabel
>
{group.items[0].provider.name}
</SwitchV2>
</Switch>
</div>
</div>
<SettingsListV2>
@@ -240,13 +243,13 @@ export const DialogManageModelsV2: Component = () => {
{(item) => (
<SettingsRowV2 title={item.name} description="">
<div>
<SwitchV2
<Switch
checked={local.model.visible({ modelID: item.id, providerID: item.provider.id })}
onChange={(checked) => setModelVisibility(item, checked)}
hideLabel
>
{item.name}
</SwitchV2>
</Switch>
</div>
</SettingsRowV2>
)}
@@ -260,6 +263,6 @@ export const DialogManageModelsV2: Component = () => {
</div>
</div>
</DialogBody>
</DialogV2>
</Dialog>
)
}
@@ -86,12 +86,12 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
<Show
when={isLast()}
fallback={
<Button variant="secondary" size="large" onClick={handleNext}>
<Button variant="neutral" size="large" onClick={handleNext}>
{language.t("dialog.releaseNotes.action.next")}
</Button>
}
>
<Button variant="primary" size="large" onClick={handleClose}>
<Button variant="contrast" size="large" onClick={handleClose}>
{language.t("dialog.releaseNotes.action.getStarted")}
</Button>
</Show>
@@ -1,8 +1,8 @@
import "@pierre/trees/web-components"
import { FileTree } from "@pierre/trees"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { Button } from "@opencode-ai/ui/button"
import { TextInput } from "@opencode-ai/ui/text-input"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createEffect, createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js"
import { useGlobal } from "@/context/global"
@@ -28,7 +28,7 @@ import {
pickerRoot,
} from "./directory-picker-domain"
import "./dialog-select-directory-v2.css"
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
import { Divider } from "@opencode-ai/ui/divider"
import { getFilename } from "@opencode-ai/util/path"
interface DialogSelectDirectoryV2Props {
@@ -293,10 +293,10 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
<DialogHeader>
<DialogTitle>{props.title ?? language.t("command.project.open")}</DialogTitle>
</DialogHeader>
<DividerV2 />
<Divider />
<DialogBody class="directory-picker-v2-body pt-4!">
<div class="directory-picker-v2-path" ref={pathArea}>
<TextInputV2
<TextInput
value={input()}
autofocus
autocomplete="off"
@@ -318,15 +318,15 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
onKeyDown={handleInputKey}
/>
<div class="directory-picker-v2-actions">
<ButtonV2 size="small" variant="ghost" onClick={() => void navigate(home())}>
<Button size="small" variant="ghost" onClick={() => void navigate(home())}>
~
</ButtonV2>
<ButtonV2 size="small" variant="ghost" onClick={() => void navigate(pickerRoot(root()) || root())}>
</Button>
<Button size="small" variant="ghost" onClick={() => void navigate(pickerRoot(root()) || root())}>
{language.t("dialog.directory.root")}
</ButtonV2>
<ButtonV2 size="small" variant="ghost" onClick={() => void navigate(pickerParent(root()))}>
</Button>
<Button size="small" variant="ghost" onClick={() => void navigate(pickerParent(root()))}>
{language.t("dialog.directory.parent")}
</ButtonV2>
</Button>
</div>
<Show when={suggestionsOpen() && currentSuggestions().length > 0}>
<div id="directory-picker-v2-suggestions" role="listbox" class="directory-picker-v2-suggestions">
@@ -379,12 +379,12 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
<div class="directory-picker-v2-selection">{policy.result(root(), selected(), rootValid())}</div>
</DialogBody>
<DialogFooter>
<ButtonV2 variant="neutral" onClick={() => dialog.close()}>
<Button variant="neutral" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2 variant="contrast" disabled={!policy.result(root(), selected(), rootValid())} onClick={resolve}>
</Button>
<Button variant="contrast" disabled={!policy.result(root(), selected(), rootValid())} onClick={resolve}>
{action[policy.action]}
</ButtonV2>
</Button>
</DialogFooter>
</Dialog>
)
@@ -1,187 +0,0 @@
import { Dialog } from "@opencode-ai/ui/dialog"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { List } from "@opencode-ai/ui/list"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createMemo, createSignal, lazy, Match, Show, Switch } from "solid-js"
import { formatKeybind } from "@/context/command"
import { useServerSDK } from "@/context/server-sdk"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useSessionLayout } from "@/pages/session/session-layout"
import { decode64 } from "@/utils/base64"
import { getRelativeTime } from "@/utils/time"
import {
createCommandPaletteFileEntry,
createCommandPaletteFileOpener,
createCommandPaletteModel,
uniqueCommandPaletteEntries,
type CommandPaletteEntry,
} from "./command-palette"
import { DialogCommandPaletteV2 } from "./dialog-command-palette-v2"
const DialogSelectFileV2 = lazy(() =>
import("./dialog-select-directory-v2").then((module) => ({ default: module.DialogSelectDirectoryV2 })),
)
type DialogSelectFileMode = "all" | "files"
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
const platform = usePlatform()
const filesOnly = () => props.mode === "files"
if (!filesOnly()) {
return <DialogCommandPaletteV2 onOpenFile={props.onOpenFile} />
}
if (filesOnly() && platform.platform === "desktop") {
return <DialogSelectFileDesktopV2 onOpenFile={props.onOpenFile} />
}
return <DialogSelectFileLegacy filesOnly={filesOnly} onOpenFile={props.onOpenFile} />
}
function DialogSelectFileDesktopV2(props: { onOpenFile?: (path: string) => void }) {
const language = useLanguage()
const serverSDK = useServerSDK()
const { params } = useSessionLayout()
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
return (
<DialogSelectFileV2
server={serverSDK.server}
mode="file"
start={projectDirectory()}
title={language.t("session.header.searchFiles")}
onSelect={(result) => {
if (typeof result !== "string") return
openFile(result)
}}
/>
)
}
function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?: (path: string) => void }) {
const palette = createCommandPaletteModel(props)
const [grouped, setGrouped] = createSignal(false)
const items = async (text: string) => {
const query = text.trim()
setGrouped(query.length > 0)
if (!query && props.filesOnly()) {
const loaded = palette.file.tree.state("")?.loaded
const pending = loaded ? Promise.resolve() : palette.file.tree.list("")
const next = uniqueCommandPaletteEntries([...palette.recentFileEntries(), ...palette.rootFileEntries()])
if (loaded || next.length > 0) {
void pending
return next
}
await pending
return uniqueCommandPaletteEntries([...palette.recentFileEntries(), ...palette.rootFileEntries()])
}
if (!query) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
if (props.filesOnly()) {
const files = await palette.file.searchFiles(query)
const category = palette.language.t("palette.group.files")
return files.map((path) => createCommandPaletteFileEntry(path, category))
}
const [files, nextSessions] = await Promise.all([
palette.file.searchFiles(query),
Promise.resolve(palette.sessions(query)),
])
const category = palette.language.t("palette.group.files")
const entries = files.map((path) => createCommandPaletteFileEntry(path, category))
return [...palette.commandEntries(), ...nextSessions, ...entries]
}
return (
<Dialog class="pt-3 pb-0 !max-h-[480px]" transition>
<List
class="px-3"
search={{
placeholder: props.filesOnly()
? palette.language.t("session.header.searchFiles")
: palette.language.t("palette.search.placeholder"),
autofocus: true,
hideIcon: true,
}}
emptyMessage={palette.language.t("palette.empty")}
loadingMessage={palette.language.t("common.loading")}
items={items}
key={(item) => item.id}
filterKeys={["title", "description", "category"]}
skipFilter={(item) => item.type === "file"}
groupBy={grouped() ? (item) => item.category : () => ""}
onMove={(item: CommandPaletteEntry | undefined) => palette.highlight(item)}
onSelect={(item: CommandPaletteEntry | undefined) => palette.select(item)}
>
{(item) => (
<Switch
fallback={
<div class="w-full flex items-center justify-between rounded-md pl-1">
<div class="flex items-center gap-x-3 grow min-w-0">
<FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular">
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(item.path ?? "")}
</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path ?? "")}</span>
</div>
</div>
</div>
}
>
<Match when={item.type === "command"}>
<div class="w-full flex items-center justify-between gap-4">
<div class="flex items-center gap-2 min-w-0">
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
<Show when={item.description}>
<span class="text-14-regular text-text-weak truncate">{item.description}</span>
</Show>
</div>
<Show when={item.keybind}>
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "", palette.language.t)}</Keybind>
</Show>
</div>
</Match>
<Match when={item.type === "session"}>
<div class="w-full flex items-center justify-between rounded-md pl-1">
<div class="flex items-center gap-x-3 grow min-w-0">
<Icon name="bubble-5" size="small" class="shrink-0 text-icon-weak" />
<div class="flex items-center gap-2 min-w-0">
<span
class="text-14-regular text-text-strong truncate"
classList={{ "opacity-70": !!item.archived }}
>
{item.title}
</span>
<Show when={item.description}>
<span
class="text-14-regular text-text-weak truncate"
classList={{ "opacity-70": !!item.archived }}
>
{item.description}
</span>
</Show>
</div>
</div>
<Show when={item.updated}>
<span class="text-12-regular text-text-weak whitespace-nowrap ml-2">
{getRelativeTime(new Date(item.updated!).toISOString(), palette.language.t)}
</span>
</Show>
</div>
</Match>
</Switch>
)}
</List>
</Dialog>
)
}
@@ -1,7 +1,7 @@
import { Component, createMemo, Show } from "solid-js"
import { useData } from "@/context/server"
import { useWorkspaceLocation } from "@/context/location"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Dialog, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Switch } from "@opencode-ai/ui/switch"
import { useLanguage } from "@/context/language"
@@ -31,65 +31,71 @@ export const DialogSelectMcp: Component = () => {
const totalCount = createMemo(() => items().length)
return (
<Dialog
title={language.t("dialog.mcp.title")}
description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
>
<List
class="px-3"
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.mcp.empty")}
key={(x) => x?.name ?? ""}
items={items}
filterKeys={["name", "status"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
onSelect={(x) => {
if (!x || x.status === "pending" || toggle.isPending) return
toggle.mutate(x.name)
}}
>
{(i) => {
const mcpStatus = () =>
data.location.mcp.server.list({ directory: sdk().directory })?.find((server) => server.name === i.name)
?.status
const status = () => mcpStatus()?.status
const statusLabel = () => {
const key = status() ? statusLabels[status() as keyof typeof statusLabels] : undefined
if (!key) return
return language.t(key)
}
const error = () => {
const s = mcpStatus()
if (s?.status === "failed") return s.error
}
const enabled = () => status() === "connected"
return (
<div class="w-full flex items-center justify-between gap-x-3">
<div class="flex flex-col gap-0.5 min-w-0">
<div class="flex items-center gap-2">
<span class="truncate">{i.name}</span>
<Show when={statusLabel()}>
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
<Dialog>
<DialogHeader>
<DialogTitleGroup
title={language.t("dialog.mcp.title")}
description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
/>
</DialogHeader>
<DialogBody>
<List
class="px-3"
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.mcp.empty")}
key={(x) => x?.name ?? ""}
items={items}
filterKeys={["name", "status"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
onSelect={(x) => {
if (!x || x.status === "pending" || toggle.isPending) return
toggle.mutate(x.name)
}}
>
{(i) => {
const mcpStatus = () =>
data.location.mcp.server.list({ directory: sdk().directory })?.find((server) => server.name === i.name)
?.status
const status = () => mcpStatus()?.status
const statusLabel = () => {
const key = status() ? statusLabels[status() as keyof typeof statusLabels] : undefined
if (!key) return
return language.t(key)
}
const error = () => {
const s = mcpStatus()
if (s?.status === "failed") return s.error
}
const enabled = () => status() === "connected"
return (
<div class="w-full flex items-center justify-between gap-x-3">
<div class="flex flex-col gap-0.5 min-w-0">
<div class="flex items-center gap-2">
<span class="truncate">{i.name}</span>
<Show when={statusLabel()}>
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
</Show>
</div>
<Show when={error()}>
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
</Show>
</div>
<Show when={error()}>
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
</Show>
<div onClick={(e) => e.stopPropagation()}>
<Switch
appearance="standard"
checked={enabled()}
disabled={status() === "pending" || (toggle.isPending && toggle.variables === i.name)}
onChange={() => {
if (toggle.isPending) return
toggle.mutate(i.name)
}}
/>
</div>
</div>
<div onClick={(e) => e.stopPropagation()}>
<Switch
checked={enabled()}
disabled={status() === "pending" || (toggle.isPending && toggle.variables === i.name)}
onChange={() => {
if (toggle.isPending) return
toggle.mutate(i.name)
}}
/>
</div>
</div>
)
}}
</List>
)
}}
</List>
</DialogBody>
</Dialog>
)
}
@@ -39,7 +39,7 @@ function SelectModelWithoutProviders() {
onMount(open)
return (
<Button variant="secondary" onClick={open}>
<Button variant="neutral" onClick={open}>
Open select model dialog
</Button>
)
@@ -1,8 +1,8 @@
import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { DialogBody, DialogHeader, DialogTitle, Dialog } from "@opencode-ai/ui/dialog"
import { Badge } from "@opencode-ai/ui/badge"
import { Icon } from "@opencode-ai/ui/icon"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme } from "@opencode-ai/ui/theme"
import { createMemo, onCleanup, onMount, type Component, For, Show } from "solid-js"
@@ -66,7 +66,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
})
return (
<DialogV2
<Dialog
fit
containerClass="!h-auto max-h-[calc(100vh_-_16px)] !w-[min(calc(100vw_-_16px),640px)]"
class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]"
@@ -84,7 +84,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
</div>
<For each={freeModels()}>
{(item) => (
<TooltipV2
<Tooltip
class="w-full"
placement="right-start"
gutter={6}
@@ -105,15 +105,15 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
onClick={() => selectModel(item)}
>
<span class="min-w-0 truncate">{displayModelName(item.name)}</span>
<Tag class="shrink-0">{language.t("model.tag.free")}</Tag>
<Badge class="shrink-0">{language.t("model.tag.free")}</Badge>
<Show when={item.latest}>
<Tag class="shrink-0">{language.t("model.tag.latest")}</Tag>
<Badge class="shrink-0">{language.t("model.tag.latest")}</Badge>
</Show>
<Show when={currentKey() === modelKey(item)}>
<Icon name="check" class="ml-auto size-4 shrink-0 text-v2-icon-icon-base" />
</Show>
</button>
</TooltipV2>
</Tooltip>
)}
</For>
</div>
@@ -172,6 +172,6 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
</div>
</div>
</DialogBody>
</DialogV2>
</Dialog>
)
}
@@ -1,9 +1,9 @@
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { List, type ListRef } from "@opencode-ai/ui/list"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tag } from "@opencode-ai/ui/tag"
import { Badge } from "@opencode-ai/ui/badge"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { type Component, Show } from "solid-js"
import { useLocal } from "@/context/local"
@@ -40,108 +40,113 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
}
return (
<Dialog
title={language.t("dialog.model.select.title")}
class="overflow-y-auto [&_[data-slot=dialog-body]]:overflow-visible [&_[data-slot=dialog-body]]:flex-none"
>
<div class="flex flex-col gap-3 px-2.5" onKeyDown={handleKeyDown}>
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
<List
class="px-3 [&_[data-slot=list-scroll]]:overflow-visible"
ref={(ref) => (listRef = ref)}
items={model.list}
current={model.current()}
key={(x) => `${x.provider.id}:${x.id}`}
itemWrapper={(item, node) => (
<Tooltip
class="w-full"
placement="right-start"
gutter={12}
value={
<ModelTooltip
model={item}
latest={item.latest}
free={item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)}
/>
}
>
{node}
</Tooltip>
)}
onSelect={(x) => {
model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
recent: true,
})
dialog.close()
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-2.5">
<span>{i.name}</span>
<Tag>{language.t("model.tag.free")}</Tag>
<Show when={i.latest}>
<Tag>{language.t("model.tag.latest")}</Tag>
</Show>
</div>
)}
</List>
</div>
<div class="px-1.5 pb-1.5">
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
<div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div>
<div class="w-full">
<List
class="w-full px-3"
key={(p) => p.id}
items={providers.popular}
activeIcon="plus-small"
sortBy={(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)
}}
onSelect={(x) => {
if (!x) return
connect(x.id)
}}
<Dialog class="overflow-y-auto [&_[data-slot=dialog-body]]:overflow-visible [&_[data-slot=dialog-body]]:flex-none">
<DialogHeader>
<DialogTitle>{language.t("dialog.model.select.title")}</DialogTitle>
</DialogHeader>
<DialogBody>
<div class="flex flex-col gap-3 px-2.5" onKeyDown={handleKeyDown}>
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
<List
class="px-3 [&_[data-slot=list-scroll]]:overflow-visible"
ref={(ref) => (listRef = ref)}
items={model.list}
current={model.current()}
key={(x) => `${x.provider.id}:${x.id}`}
itemWrapper={(item, node) => (
<Tooltip
appearance="standard"
class="w-full"
placement="right-start"
gutter={12}
value={
<ModelTooltip
model={item}
latest={item.latest}
free={item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)}
/>
}
>
{(i) => (
<div class="w-full flex items-center gap-x-3">
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
</Show>
<Show when={i.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
<Show when={i.id === "opencode-go"}>
<>
{node}
</Tooltip>
)}
onSelect={(x) => {
model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
recent: true,
})
dialog.close()
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-2.5">
<span>{i.name}</span>
<Badge appearance="standard">{language.t("model.tag.free")}</Badge>
<Show when={i.latest}>
<Badge appearance="standard">{language.t("model.tag.latest")}</Badge>
</Show>
</div>
)}
</List>
</div>
<div class="px-1.5 pb-1.5">
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
<div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div>
<div class="w-full">
<List
class="w-full px-3"
key={(p) => p.id}
items={providers.popular}
activeIcon="plus-small"
sortBy={(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)
}}
onSelect={(x) => {
if (!x) return
connect(x.id)
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-3">
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<div class="text-14-regular text-text-weak">
{language.t("dialog.provider.opencodeGo.tagline")}
{language.t("dialog.provider.opencode.tagline")}
</div>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</>
</Show>
<Show when={i.id === "anthropic"}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
</Show>
</div>
)}
</List>
<Button
variant="ghost"
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
icon="dot-grid"
onClick={all}
>
{language.t("dialog.provider.viewAll")}
</Button>
</Show>
<Show when={i.id === "opencode"}>
<Badge appearance="standard">{language.t("dialog.provider.tag.recommended")}</Badge>
</Show>
<Show when={i.id === "opencode-go"}>
<>
<div class="text-14-regular text-text-weak">
{language.t("dialog.provider.opencodeGo.tagline")}
</div>
<Badge appearance="standard">{language.t("dialog.provider.tag.recommended")}</Badge>
</>
</Show>
<Show when={i.id === "anthropic"}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
</Show>
</div>
)}
</List>
<Button
variant="ghost"
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
icon="dot-grid"
onClick={all}
>
{language.t("dialog.provider.viewAll")}
</Button>
</div>
</div>
</div>
</div>
</div>
</DialogBody>
</Dialog>
)
}
@@ -5,16 +5,14 @@ import { useLocal } from "@/context/local"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { popularProviders } from "@/hooks/use-providers"
import { Button } from "@opencode-ai/ui/button"
import { Badge } from "@opencode-ai/ui/badge"
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { Tag } from "@opencode-ai/ui/tag"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { Tag as TagV2 } from "@opencode-ai/ui/v2/badge-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Menu } from "@opencode-ai/ui/menu"
import { ModelTooltip } from "./model-tooltip"
import { useLanguage } from "@/context/language"
import { decode64 } from "@/utils/base64"
@@ -81,6 +79,7 @@ const ModelList: Component<{
}}
itemWrapper={(item, node) => (
<Tooltip
appearance="standard"
class="w-full"
placement="right-start"
gutter={12}
@@ -101,10 +100,10 @@ const ModelList: Component<{
<div class="w-full flex items-center gap-x-2 text-13-regular">
<span class="truncate">{i.name}</span>
<Show when={isFree(i.provider.id, i.cost)}>
<Tag>{language.t("model.tag.free")}</Tag>
<Badge appearance="standard">{language.t("model.tag.free")}</Badge>
</Show>
<Show when={i.latest}>
<Tag>{language.t("model.tag.latest")}</Tag>
<Badge appearance="standard">{language.t("model.tag.latest")}</Badge>
</Show>
</div>
)}
@@ -193,21 +192,19 @@ export function ModelSelectorPopover(props: {
class="p-1"
action={
<div class="flex items-center gap-1">
<Tooltip placement="top" value={language.t("command.provider.connect")}>
<Tooltip appearance="standard" placement="top" value={language.t("command.provider.connect")}>
<IconButton
icon="plus-small"
icon={<Icon name="plus-small" />}
variant="ghost"
iconSize="normal"
class="size-6"
aria-label={language.t("command.provider.connect")}
onClick={handleConnectProvider}
/>
</Tooltip>
<Tooltip placement="top" value={language.t("dialog.model.manage")}>
<Tooltip appearance="standard" placement="top" value={language.t("dialog.model.manage")}>
<IconButton
icon="sliders"
icon={<Icon name="sliders" />}
variant="ghost"
iconSize="normal"
class="size-6"
aria-label={language.t("dialog.model.manage")}
onClick={handleManage}
@@ -373,10 +370,10 @@ function ModelSelectorPopoverV2View(props: {
})
return (
<MenuV2 open={store.open} modal={false} placement="top-start" gutter={6} onOpenChange={setOpen}>
<MenuV2.Trigger as={props.trigger} />
<MenuV2.Portal>
<MenuV2.Content
<Menu open={store.open} modal={false} placement="top-start" gutter={6} onOpenChange={setOpen}>
<Menu.Trigger as={props.trigger} />
<Menu.Portal>
<Menu.Content
ref={(element: HTMLDivElement) => (contentRef = element)}
class="w-[284px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 !p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none"
onPointerDownOutside={dismiss.preventTriggerRestore}
@@ -449,14 +446,14 @@ function ModelSelectorPopoverV2View(props: {
>
<For each={groups()}>
{(group) => (
<MenuV2.Group>
<MenuV2.GroupLabel class="gap-2 px-3">
<Menu.Group>
<Menu.GroupLabel class="gap-2 px-3">
<span class="min-w-0 truncate">{group.items[0].provider.name}</span>
</MenuV2.GroupLabel>
<MenuV2.RadioGroup value={props.current}>
</Menu.GroupLabel>
<Menu.RadioGroup value={props.current}>
<For each={group.items}>
{(item) => (
<TooltipV2
<Tooltip
class="w-full"
placement="right-start"
gutter={6}
@@ -470,7 +467,7 @@ function ModelSelectorPopoverV2View(props: {
/>
}
>
<MenuV2.RadioItem
<Menu.RadioItem
value={modelKey(item)}
data-option-key={modelKey(item)}
data-selected-model={props.current === modelKey(item) ? true : undefined}
@@ -484,17 +481,17 @@ function ModelSelectorPopoverV2View(props: {
>
<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>
<Badge class="shrink-0">{language.t("model.tag.free")}</Badge>
</Show>
<Show when={item.latest}>
<TagV2 class="shrink-0">{language.t("model.tag.latest")}</TagV2>
<Badge class="shrink-0">{language.t("model.tag.latest")}</Badge>
</Show>
</MenuV2.RadioItem>
</TooltipV2>
</Menu.RadioItem>
</Tooltip>
)}
</For>
</MenuV2.RadioGroup>
</MenuV2.Group>
</Menu.RadioGroup>
</Menu.Group>
)}
</For>
</Show>
@@ -502,7 +499,7 @@ function ModelSelectorPopoverV2View(props: {
</ScrollView>
<div class="h-px bg-v2-border-border-muted" />
<div class="flex flex-col p-0.5">
<MenuV2.Item
<Menu.Item
data-option-key={manageKey}
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === manageKey }}
onMouseEnter={() => {
@@ -513,11 +510,11 @@ function ModelSelectorPopoverV2View(props: {
>
<Icon name="outline-sliders" size="small" />
<span class="min-w-0 flex-1 truncate leading-5">{language.t("dialog.model.manage")}</span>
</MenuV2.Item>
</Menu.Item>
</div>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</Menu.Content>
</Menu.Portal>
</Menu>
)
}
@@ -540,18 +537,19 @@ export const DialogSelectModel: Component<{ provider?: string; model?: ModelStat
}
return (
<Dialog
title={language.t("dialog.model.select.title")}
action={
<Dialog>
<DialogHeader hideClose>
<DialogTitle>{language.t("dialog.model.select.title")}</DialogTitle>
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={provider}>
{language.t("command.provider.connect")}
</Button>
}
>
<ModelList provider={props.provider} model={props.model} onSelect={() => dialog.close()} />
<Button variant="ghost" class="ml-3 mt-5 mb-6 text-text-base self-start" onClick={manage}>
{language.t("dialog.model.manage")}
</Button>
</DialogHeader>
<DialogBody>
<ModelList provider={props.provider} model={props.model} onSelect={() => dialog.close()} />
<Button variant="ghost" class="ml-3 mt-5 mb-6 text-text-base self-start" onClick={manage}>
{language.t("dialog.model.manage")}
</Button>
</DialogBody>
</Dialog>
)
}
@@ -1,5 +1,6 @@
import { Button } from "@opencode-ai/ui/button"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Menu } from "@opencode-ai/ui/menu"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { List } from "@opencode-ai/ui/list"
import { TextField } from "@opencode-ai/ui/text-field"
@@ -156,49 +157,47 @@ export function ServerConnectionList(props: {
/>
<div class="flex items-center justify-center gap-4 pl-4">
<Show when={i.type === "http"}>
<DropdownMenu>
<DropdownMenu.Trigger
<Menu appearance="standard">
<Menu.Trigger
as={IconButton}
icon="dot-grid"
icon={<Icon name="dot-grid" />}
variant="ghost"
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
onClick={(e: MouseEvent) => e.stopPropagation()}
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content class="mt-1">
<DropdownMenu.Item
<Menu.Portal>
<Menu.Content class="mt-1">
<Menu.Item
onSelect={() => {
if (i.type !== "http") return
props.onEdit(i)
}}
>
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
{language.t("dialog.server.menu.edit")}
</Menu.Item>
<Show when={props.domain.defaults.available() && props.domain.defaults.key() !== key}>
<DropdownMenu.Item onSelect={() => props.domain.defaults.set(key)}>
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.default")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<Menu.Item onSelect={() => props.domain.defaults.set(key)}>
{language.t("dialog.server.menu.default")}
</Menu.Item>
</Show>
<Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
<DropdownMenu.Item onSelect={() => props.domain.defaults.set(null)}>
<DropdownMenu.ItemLabel>
{language.t("dialog.server.menu.defaultRemove")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<Menu.Item onSelect={() => props.domain.defaults.set(null)}>
{language.t("dialog.server.menu.defaultRemove")}
</Menu.Item>
</Show>
<Show when={props.domain.connection.canRemove(key)}>
<DropdownMenu.Separator />
<DropdownMenu.Item
<Menu.Separator />
<Menu.Item
onSelect={() => props.domain.connection.remove(key)}
class="text-text-on-critical-base hover:bg-surface-critical-weak"
>
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
{language.t("dialog.server.menu.delete")}
</Menu.Item>
</Show>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</Menu.Content>
</Menu.Portal>
</Menu>
</Show>
</div>
</div>
@@ -208,7 +207,7 @@ export function ServerConnectionList(props: {
<div class="shrink-0 pb-5">
<Button
variant="secondary"
variant="neutral"
icon="plus-small"
size="large"
onClick={props.onAdd}
@@ -244,7 +243,7 @@ export function ServerConnectionForm(props: { form: ServerConnectionFormControll
/>
<div class="shrink-0 pb-5">
<Button
variant="primary"
variant="contrast"
size="large"
onClick={props.form.submit}
disabled={props.form.state.busy()}
@@ -2,7 +2,7 @@ import { usePlatform } from "@/context/platform"
import { useLanguage } from "@/context/language"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Dialog, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/dialog"
import { JSX } from "solid-js"
export type DialogGoUpsellProps = {
@@ -30,17 +30,22 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) {
}
return (
<Dialog title={props.title} description={props.description} fit>
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
<div class="flex justify-end gap-2">
<Button variant="ghost" size="large" onClick={dismiss}>
{language.t("dialog.usageExceeded.dontShowAgain")}
</Button>
<Button variant="primary" size="large" onClick={runAction}>
{props.actionLabel}
</Button>
<Dialog fit>
<DialogHeader>
<DialogTitleGroup title={props.title} description={props.description} />
</DialogHeader>
<DialogBody>
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
<div class="flex justify-end gap-2">
<Button variant="ghost" size="large" onClick={dismiss}>
{language.t("dialog.usageExceeded.dontShowAgain")}
</Button>
<Button variant="contrast" size="large" onClick={runAction}>
{props.actionLabel}
</Button>
</div>
</div>
</div>
</DialogBody>
</Dialog>
)
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { useFile } from "@/context/file"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import "@opencode-ai/ui/v2/file-tree-v2.css"
import "@opencode-ai/ui/file-tree.css"
import {
createEffect,
createMemo,
@@ -13,7 +13,7 @@ import {
} from "solid-js"
import { Dynamic } from "solid-js/web"
import type { FileNode } from "@/types"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { Icon } from "@opencode-ai/ui/icon"
import { pathToFileUrl, withFileDragImage, type Kind } from "@/components/file-tree"
import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
import {
@@ -1,6 +1,6 @@
import { Icon } from "@opencode-ai/ui/icon"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
import { Switch } from "@opencode-ai/ui/switch"
import { Tabs } from "@opencode-ai/ui/tabs"
import { type Component, For, Show, createEffect, createMemo, createResource, createSignal } from "solid-js"
import { useLanguage } from "@/context/language"
import { useMcpToggle } from "@/context/mcp"
@@ -149,15 +149,15 @@ export const ProjectSettingsExtensions: Component = () => {
<span>{language.t("project.settings.extensions.description")}</span>
</div>
<TabsV2 variant="pill" defaultValue="mcps" class="project-settings-extension-tabs">
<TabsV2.List>
<TabsV2.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</TabsV2.Trigger>
<TabsV2.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</TabsV2.Trigger>
<TabsV2.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</TabsV2.Trigger>
<Tabs variant="pill" defaultValue="mcps" class="project-settings-extension-tabs">
<Tabs.List>
<Tabs.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</Tabs.Trigger>
<Tabs.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</Tabs.Trigger>
<Tabs.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</Tabs.Trigger>
{/* TODO: Restore LSP status when V2 exposes it. */}
</TabsV2.List>
</Tabs.List>
<TabsV2.Content value="mcps">
<Tabs.Content value="mcps">
<div class="project-settings-extension-section">
<div class="project-settings-extension-section-header">
<span>{language.t("project.settings.extensions.added")}</span>
@@ -168,9 +168,9 @@ export const ProjectSettingsExtensions: Component = () => {
</Show>
<SharedSection count={globalMcpNames().length}>{mcpRows(globalMcpNames())}</SharedSection>
</div>
</TabsV2.Content>
</Tabs.Content>
<TabsV2.Content value="plugins">
<Tabs.Content value="plugins">
<div class="project-settings-extension-section">
<div class="project-settings-extension-section-header">
<span>{language.t("project.settings.extensions.added")}</span>
@@ -181,9 +181,9 @@ export const ProjectSettingsExtensions: Component = () => {
</Show>
<SharedSection count={globalPlugins().length}>{pluginRows(globalPlugins())}</SharedSection>
</div>
</TabsV2.Content>
</Tabs.Content>
<TabsV2.Content value="skills">
<Tabs.Content value="skills">
<div class="project-settings-extension-section">
<div class="project-settings-extension-section-header">
<span>{language.t("project.settings.extensions.added")}</span>
@@ -196,8 +196,8 @@ export const ProjectSettingsExtensions: Component = () => {
</Show>
<SharedSection count={serverSkills().length}>{skillRows(serverSkills())}</SharedSection>
</div>
</TabsV2.Content>
</TabsV2>
</Tabs.Content>
</Tabs>
</div>
)
}
+11 -11
View File
@@ -1,10 +1,10 @@
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import type { ReferenceInfo } from "@opencode-ai/client/promise"
import { createEffect, createMemo, on, Show } from "solid-js"
import { ModelSelectorPopoverV2 } from "@/components/dialog-select-model"
@@ -503,20 +503,20 @@ function PromptInputV2ModelControl(props: {
)
return (
<Show when={!props.loading}>
<TooltipV2
<Tooltip
placement="top"
gutter={4}
value={
<>
{props.title}
<KeybindV2 keys={props.keybind} variant="neutral" />
<Keybind keys={props.keybind} variant="neutral" />
</>
}
>
<Show
when={props.paid}
fallback={
<ButtonV2
<Button
data-action="prompt-model"
data-control-type="dialog"
variant="ghost-muted"
@@ -527,13 +527,13 @@ function PromptInputV2ModelControl(props: {
onClick={props.onUnpaidClick}
>
{content()}
</ButtonV2>
</Button>
}
>
<ModelSelectorPopoverV2
model={props.model}
trigger={(triggerProps) => (
<ButtonV2
<Button
{...triggerProps}
variant="ghost-muted"
size="normal"
@@ -544,12 +544,12 @@ function PromptInputV2ModelControl(props: {
data-control-type="popover"
>
{content()}
</ButtonV2>
</Button>
)}
onClose={props.onClose}
/>
</Show>
</TooltipV2>
</Tooltip>
</Show>
)
}
@@ -9,10 +9,9 @@ import {
type ComponentProps,
} from "solid-js"
import { createStore } from "solid-js/store"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Menu } from "@opencode-ai/ui/menu"
import { Icon } from "@opencode-ai/ui/icon"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
import { getProjectAvatarVariant } from "@/context/layout"
import { useLanguage } from "@/context/language"
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
@@ -277,7 +276,8 @@ export function PromptProjectSelector(props: {
})
return (
<DropdownMenu
<Menu
appearance="standard"
open={triggerReady() && props.controller.open()}
placement={props.placement ?? "bottom"}
gutter={4}
@@ -287,9 +287,9 @@ export function PromptProjectSelector(props: {
props.controller.setOpen(open)
}}
>
<DropdownMenu.Trigger as={ProjectTrigger} ref={setTriggerRef} controller={props.controller} />
<DropdownMenu.Portal>
<DropdownMenu.Content
<Menu.Trigger as={ProjectTrigger} ref={setTriggerRef} controller={props.controller} />
<Menu.Portal>
<Menu.Content
ref={contentRef}
id="prompt-project-menu"
class="w-[243px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none [&[data-closed]]:!animate-none"
@@ -360,13 +360,13 @@ export function PromptProjectSelector(props: {
<Show
when={props.controller.servers().length > 1}
fallback={
<DropdownMenu.RadioGroup value={selectedValue()}>
<Menu.RadioGroup value={selectedValue()}>
<For each={props.controller.projects()}>
{(project) => (
<ProjectItem project={project} controller={props.controller} onSelect={selectProject} />
)}
</For>
</DropdownMenu.RadioGroup>
</Menu.RadioGroup>
}
>
<For
@@ -381,7 +381,7 @@ export function PromptProjectSelector(props: {
<div class="flex h-7 select-none items-center pl-1.5 pr-3 text-[11px] font-[530] leading-none tracking-[0.05px] text-v2-text-text-faint">
{server!.name}
</div>
<DropdownMenu.RadioGroup value={selectedValue()}>
<Menu.RadioGroup value={selectedValue()}>
<For
each={props.controller.projects().filter((project) => project.server?.key === server!.key)}
>
@@ -389,7 +389,7 @@ export function PromptProjectSelector(props: {
<ProjectItem project={project} controller={props.controller} onSelect={selectProject} />
)}
</For>
</DropdownMenu.RadioGroup>
</Menu.RadioGroup>
</div>
)}
</For>
@@ -408,8 +408,8 @@ export function PromptProjectSelector(props: {
/>
}
>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
<Menu.Sub>
<Menu.SubTrigger
id={props.controller.actionKey()}
data-option-key={props.controller.actionKey()}
class={projectActionClass}
@@ -419,24 +419,23 @@ export function PromptProjectSelector(props: {
onMouseEnter={() => props.controller.setActive(props.controller.actionKey())}
>
<Icon name="plus" size="small" />
<span data-slot="dropdown-menu-item-label" class="min-w-0 flex-1 truncate leading-5">
<span class="min-w-0 flex-1 truncate leading-5">
{props.controller.labels.add()}
</span>
<Icon name="chevron-right" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</DropdownMenu.SubTrigger>
<DropdownMenu.Portal>
<DropdownMenu.SubContent class="min-w-[180px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 p-0.5 shadow-[var(--v2-elevation-floating)] focus:outline-none">
</Menu.SubTrigger>
<Menu.Portal>
<Menu.SubContent class="min-w-[180px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 p-0.5 shadow-[var(--v2-elevation-floating)] focus:outline-none">
<For each={props.controller.servers()}>
{(server) => <ServerAction server={server!} onSelect={selectAction} />}
</For>
</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>
</Menu.SubContent>
</Menu.Portal>
</Menu.Sub>
</Show>
</div>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</Menu.Content>
</Menu.Portal>
</Menu>
)
}
@@ -507,7 +506,7 @@ function ProjectItem(props: {
}) {
const key = () => props.controller.projectKey(props.project)
return (
<DropdownMenu.RadioItem
<Menu.RadioItem
id={key()}
value={key()}
data-option-key={key()}
@@ -534,11 +533,8 @@ function ProjectItem(props: {
src={getProjectAvatarSource(props.project.id, props.project.icon)}
variant={getProjectAvatarVariant(props.project.icon?.color)}
/>
<DropdownMenu.ItemLabel class="min-w-0 truncate leading-5">{displayName(props.project)}</DropdownMenu.ItemLabel>
<DropdownMenu.ItemIndicator style={{ width: "14px", height: "14px", right: "12px" }}>
<IconV2 name="check" size="small" class="shrink-0 text-v2-icon-icon-base" />
</DropdownMenu.ItemIndicator>
</DropdownMenu.RadioItem>
<span class="min-w-0 truncate leading-5">{displayName(props.project)}</span>
</Menu.RadioItem>
)
}
@@ -552,7 +548,7 @@ function ProjectAction(props: {
}) {
const key = () => props.controller.actionKey(props.server)
return (
<DropdownMenu.Item
<Menu.Item
id={key()}
data-option-key={key()}
class="h-7 gap-2 rounded-sm px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base data-[highlighted]:!bg-v2-overlay-simple-overlay-hover"
@@ -573,17 +569,17 @@ function ProjectAction(props: {
onSelect={() => props.onSelect(props.server)}
>
<Icon name="plus" size="small" />
<DropdownMenu.ItemLabel class="min-w-0 truncate leading-5">
<span class="min-w-0 truncate leading-5">
{props.controller.labels.add()}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</span>
</Menu.Item>
)
}
function ServerAction(props: { server: { key: string; name: string }; onSelect: (server: string) => void }) {
return (
<DropdownMenu.Item class={projectActionClass} onSelect={() => props.onSelect(props.server.key)}>
<DropdownMenu.ItemLabel class="min-w-0 flex-1 truncate leading-5">{props.server.name}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<Menu.Item class={projectActionClass} onSelect={() => props.onSelect(props.server.key)}>
<span class="min-w-0 flex-1 truncate leading-5">{props.server.name}</span>
</Menu.Item>
)
}
@@ -1,7 +1,7 @@
import { createMemo, createSignal, For, Show } from "solid-js"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { Menu } from "@opencode-ai/ui/menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Icon } from "@opencode-ai/ui/icon"
import { getFilename } from "@opencode-ai/util/path"
import { useLanguage } from "@/context/language"
import { sameDirectory } from "@/utils/workspace"
@@ -58,7 +58,7 @@ export function PromptWorkspaceSelector(props: {
return (
<>
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
<TooltipV2
<Tooltip
placement="top"
openDelay={800}
value={
@@ -77,8 +77,8 @@ export function PromptWorkspaceSelector(props: {
contentClass={props.onboarding ? "max-w-[280px]" : undefined}
class="min-w-0"
>
<MenuV2 placement="bottom" gutter={4} onOpenChange={onOpenChange}>
<MenuV2.Trigger
<Menu placement="bottom" gutter={4} onOpenChange={onOpenChange}>
<Menu.Trigger
aria-description={language.t("session.new.workspace.trigger.tooltip")}
class="flex h-6 min-w-0 max-w-[203px] items-center gap-1.5 rounded-sm px-1.5 hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed data-[expanded]:text-v2-text-text-muted"
>
@@ -92,14 +92,14 @@ export function PromptWorkspaceSelector(props: {
/>
</Show>
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content class="w-[200px]">
<MenuV2.Group>
<MenuV2.GroupLabel>{language.t("session.new.workspace.runIn")}</MenuV2.GroupLabel>
<MenuV2.Item onSelect={() => select("main")}>
</Menu.Trigger>
<Menu.Portal>
<Menu.Content class="w-[200px]">
<Menu.Group>
<Menu.GroupLabel>{language.t("session.new.workspace.runIn")}</Menu.GroupLabel>
<Menu.Item onSelect={() => select("main")}>
<Icon name="monitor" />
<TooltipV2
<Tooltip
placement="right"
openDelay={800}
value={
@@ -113,14 +113,14 @@ export function PromptWorkspaceSelector(props: {
class="min-w-0 flex-1"
>
<span class="min-w-0 truncate">{language.t("session.new.workspace.local")}</span>
</TooltipV2>
</Tooltip>
<Show when={selected() === "main"}>
<Icon name="check" size="small" class="shrink-0" />
</Show>
</MenuV2.Item>
<MenuV2.Item onSelect={() => select("create")}>
</Menu.Item>
<Menu.Item onSelect={() => select("create")}>
<Icon name="workspace-new" />
<TooltipV2
<Tooltip
placement="right"
openDelay={800}
value={
@@ -134,25 +134,25 @@ export function PromptWorkspaceSelector(props: {
class="min-w-0 flex-1"
>
<span class="min-w-0 truncate">{language.t("workspace.new")}</span>
</TooltipV2>
</Tooltip>
<Show when={selected() === "create"}>
<Icon name="check" size="small" class="shrink-0" />
</Show>
</MenuV2.Item>
</MenuV2.Group>
</Menu.Item>
</Menu.Group>
<Show
when={props.workspaces.length > 0}
fallback={
<>
<MenuV2.Separator class="h-[0.5px]" />
<MenuV2.Item onSelect={() => (pending = { type: "viewAll" })}>
<Menu.Separator class="h-[0.5px]" />
<Menu.Item onSelect={() => (pending = { type: "viewAll" })}>
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
</MenuV2.Item>
</Menu.Item>
</>
}
>
<MenuV2.Separator class="h-[0.5px]" />
<MenuV2.Sub
<Menu.Separator class="h-[0.5px]" />
<Menu.Sub
gutter={0}
overlap
overflowPadding={8}
@@ -166,7 +166,7 @@ export function PromptWorkspaceSelector(props: {
requestAnimationFrame(() => searchInput?.focus())
}}
>
<MenuV2.SubTrigger
<Menu.SubTrigger
onKeyDown={(event) => {
if (
event.key === "ArrowRight" ||
@@ -181,9 +181,9 @@ export function PromptWorkspaceSelector(props: {
<span class="min-w-0 flex-1 truncate">
{language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")}
</span>
</MenuV2.SubTrigger>
<MenuV2.Portal>
<MenuV2.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
</Menu.SubTrigger>
<Menu.Portal>
<Menu.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
<Show when={props.workspaces.length >= 10}>
<div class="flex h-7 items-center gap-2 rounded-sm ps-3 pe-2 text-v2-icon-icon-muted">
<Icon name="magnifying-glass" size="small" class="shrink-0" />
@@ -211,27 +211,27 @@ export function PromptWorkspaceSelector(props: {
</Show>
<For each={workspaces()}>
{(workspace) => (
<MenuV2.Item onSelect={() => select(workspace)}>
<Menu.Item onSelect={() => select(workspace)}>
<Icon name="workspace-isolated" />
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
<Show when={selected() === workspace}>
<Icon name="check" size="small" class="shrink-0" />
</Show>
</MenuV2.Item>
</Menu.Item>
)}
</For>
<MenuV2.Separator class="h-[0.5px]" />
<MenuV2.Item onSelect={() => (pending = { type: "viewAll" })}>
<Menu.Separator class="h-[0.5px]" />
<Menu.Item onSelect={() => (pending = { type: "viewAll" })}>
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
</MenuV2.Item>
</MenuV2.SubContent>
</MenuV2.Portal>
</MenuV2.Sub>
</Menu.Item>
</Menu.SubContent>
</Menu.Portal>
</Menu.Sub>
</Show>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</TooltipV2>
</Menu.Content>
</Menu.Portal>
</Menu>
</Tooltip>
<PromptGitStatus branch={props.branch} from={selected() === "create"} class="ms-1" />
</>
)
@@ -255,7 +255,7 @@ export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?
return (
<Show when={label()}>
{(value) => (
<TooltipV2
<Tooltip
placement="top"
value={value()}
class={`min-w-0 max-w-[220px] ${props.class ?? ""}`}
@@ -265,7 +265,7 @@ export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?
<Icon name={icon()} size="small" class="shrink-0 text-v2-icon-icon-muted" />
<span class="min-w-0 truncate">{value()}</span>
</div>
</TooltipV2>
</Tooltip>
)}
</Show>
)
@@ -1,6 +1,6 @@
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Menu } from "@opencode-ai/ui/menu"
import { type Component, Show } from "solid-js"
import type { ServerActionsController } from "@/components/server/server-management-controller"
import { useLanguage } from "@/context/language"
@@ -59,19 +59,19 @@ export const ServerRowMenuView: Component<{
const builtin = () => ServerConnection.builtin(props.server)
const httpServer = () => (props.server.type === "http" ? props.server : undefined)
return (
<MenuV2 gutter={6} modal={false} placement="bottom-end" open={props.open} onOpenChange={props.onOpenChange}>
<MenuV2.Trigger
as={IconButtonV2}
<Menu gutter={6} modal={false} placement="bottom-end" open={props.open} onOpenChange={props.onOpenChange}>
<Menu.Trigger
as={IconButton}
variant="ghost-muted"
size="small"
icon={<IconV2 name="outline-dots" />}
icon={<Icon name="outline-dots" />}
aria-label={props.labels.more}
/>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Group>
<MenuV2.GroupLabel>{props.labels.server}</MenuV2.GroupLabel>
<MenuV2.Item
<Menu.Portal>
<Menu.Content>
<Menu.Group>
<Menu.GroupLabel>{props.labels.server}</Menu.GroupLabel>
<Menu.Item
disabled={builtin() || !httpServer()}
onSelect={() => {
const server = httpServer()
@@ -79,20 +79,20 @@ export const ServerRowMenuView: Component<{
}}
>
{props.labels.edit}
</MenuV2.Item>
</Menu.Item>
<Show when={props.canDefault && !props.isDefault}>
<MenuV2.Item onSelect={props.onSetDefault}>{props.labels.default}</MenuV2.Item>
<Menu.Item onSelect={props.onSetDefault}>{props.labels.default}</Menu.Item>
</Show>
<Show when={props.canDefault && props.isDefault}>
<MenuV2.Item onSelect={props.onRemoveDefault}>{props.labels.defaultRemove}</MenuV2.Item>
<Menu.Item onSelect={props.onRemoveDefault}>{props.labels.defaultRemove}</Menu.Item>
</Show>
<Show when={props.canRemove}>
<MenuV2.Separator />
<MenuV2.Item onSelect={props.onRemove}>{props.labels.delete}</MenuV2.Item>
<Menu.Separator />
<Menu.Item onSelect={props.onRemove}>{props.labels.delete}</Menu.Item>
</Show>
</MenuV2.Group>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</Menu.Group>
</Menu.Content>
</Menu.Portal>
</Menu>
)
}
@@ -64,6 +64,7 @@ export function ServerRow(props: ServerRowProps) {
return (
<Tooltip
appearance="standard"
class="flex-1 min-w-0"
value={tooltipValue()}
contentStyle={{ "max-width": "none", "white-space": "nowrap" }}
@@ -1,8 +1,7 @@
import { Show, createMemo, type ComponentProps, type JSX } from "solid-js"
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import { ProgressCircleV2 } from "@opencode-ai/ui/v2/progress-circle-v2"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { createMediaQuery } from "@solid-primitives/media"
import { useFile } from "@/context/file"
@@ -16,7 +15,7 @@ import { createSessionTabs } from "@/pages/session/helpers"
interface SessionContextUsageProps {
variant?: "button" | "indicator"
placement?: ComponentProps<typeof TooltipV2>["placement"]
placement?: ComponentProps<typeof Tooltip>["placement"]
}
function ContextTooltipRow(props: { name: JSX.Element; value: JSX.Element }) {
@@ -111,24 +110,21 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const circle = () => (
<div class="flex items-center justify-center">
<ProgressCircle
appearance="indicator"
size={16}
strokeWidth={2}
percentage={context()?.usage ?? 0}
style={
variant() === "indicator"
? {
"--progress-circle-background": "var(--v2-background-bg-layer-04, var(--border-weak-base))",
"--progress-circle-background-overlay": "var(--v2-overlay-simple-overlay-pressed, transparent)",
"--progress-circle-progress": "var(--v2-icon-icon-base, var(--icon-base))",
}
: undefined
}
style={{
"--progress-circle-background": "var(--v2-background-bg-layer-04, var(--border-weak-base))",
"--progress-circle-background-overlay": "var(--v2-overlay-simple-overlay-pressed, transparent)",
"--progress-circle-progress": "var(--v2-icon-icon-base, var(--icon-base))",
}}
/>
</div>
)
const circleV2 = () => (
<div class="flex items-center justify-center">
<ProgressCircleV2 percentage={context()?.usage ?? 0} />
<ProgressCircle appearance="compact" percentage={context()?.usage ?? 0} />
</div>
)
@@ -145,11 +141,11 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
return (
<Show when={params.id}>
<TooltipV2 value={tooltipValue()} placement={props.placement ?? "top"} shift={-8}>
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"} shift={-8}>
<Show
when={variant() === "indicator"}
fallback={
<IconButtonV2
<IconButton
type="button"
variant="ghost-muted"
size="large"
@@ -161,7 +157,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
>
{circle()}
</Show>
</TooltipV2>
</Tooltip>
</Show>
)
}
@@ -1,5 +1,5 @@
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { Menu } from "@opencode-ai/ui/menu"
import { Icon } from "@opencode-ai/ui/icon"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createStore } from "solid-js/store"
import { createSignal, For, Show, type ComponentProps, type JSX } from "solid-js"
@@ -17,7 +17,7 @@ export function SessionWorkspaceMenu(props: {
sessionID: string
project: Project
directory: string
placement?: ComponentProps<typeof MenuV2>["placement"]
placement?: ComponentProps<typeof Menu>["placement"]
gutter?: number
class?: string
contentClass?: string
@@ -71,57 +71,57 @@ export function SessionWorkspaceMenu(props: {
}
return (
<MenuV2
<Menu
placement={props.placement ?? "bottom-end"}
gutter={props.gutter ?? 4}
modal={false}
onOpenChange={onOpenChange}
>
<MenuV2.Trigger class={props.class} disabled={blocked()}>
<Menu.Trigger class={props.class} disabled={blocked()}>
{props.children}
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content class={`w-[200px] ${props.contentClass ?? ""}`}>
<MenuV2.Group>
<MenuV2.GroupLabel>{language.t("workspace.move.menu.title")}</MenuV2.GroupLabel>
</Menu.Trigger>
<Menu.Portal>
<Menu.Content class={`w-[200px] ${props.contentClass ?? ""}`}>
<Menu.Group>
<Menu.GroupLabel>{language.t("workspace.move.menu.title")}</Menu.GroupLabel>
<Show when={pathKey(props.directory) !== pathKey(props.project.worktree)}>
<MenuV2.Item disabled={!!store.selected || blocked()} onSelect={() => void move(props.project.worktree)}>
<Menu.Item disabled={!!store.selected || blocked()} onSelect={() => void move(props.project.worktree)}>
<Icon name="monitor" />
{language.t("session.new.workspace.local")}
</MenuV2.Item>
</Menu.Item>
</Show>
<MenuV2.Item disabled={!!store.selected || blocked()} onSelect={() => void move("create")}>
<Menu.Item disabled={!!store.selected || blocked()} onSelect={() => void move("create")}>
<Icon name="workspace-new" />
{language.t("workspace.new")}
</MenuV2.Item>
</Menu.Item>
<Show when={workspaces().length > 0}>
<MenuV2.Sub gutter={0} overlap overflowPadding={8}>
<MenuV2.SubTrigger>
<Menu.Sub gutter={0} overlap overflowPadding={8}>
<Menu.SubTrigger>
<Icon name="workspace-isolated" />
{language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")}
</MenuV2.SubTrigger>
<MenuV2.Portal>
<MenuV2.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
</Menu.SubTrigger>
<Menu.Portal>
<Menu.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
<For each={workspaces()}>
{(workspace) => (
<MenuV2.Item disabled={!!store.selected || blocked()} onSelect={() => void move(workspace)}>
<Menu.Item disabled={!!store.selected || blocked()} onSelect={() => void move(workspace)}>
<Icon name="workspace-isolated" />
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
</MenuV2.Item>
</Menu.Item>
)}
</For>
</MenuV2.SubContent>
</MenuV2.Portal>
</MenuV2.Sub>
</Menu.SubContent>
</Menu.Portal>
</Menu.Sub>
</Show>
</MenuV2.Group>
<MenuV2.Separator class="h-[0.5px] bg-v2-border-border-base" />
<MenuV2.Item onSelect={() => openWorkspaces()}>
</Menu.Group>
<Menu.Separator class="h-[0.5px] bg-v2-border-border-base" />
<Menu.Item onSelect={() => openWorkspaces()}>
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
)
}
@@ -2,10 +2,9 @@ import { For, Show } from "solid-js"
import { AppIcon } from "@opencode-ai/ui/app-icon"
import { Icon } from "@opencode-ai/ui/icon"
import { Spinner } from "@opencode-ai/ui/spinner"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { SplitButtonV2, SplitButtonV2Action, SplitButtonV2MenuTrigger } from "@opencode-ai/ui/v2/split-button-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Menu } from "@opencode-ai/ui/menu"
import { SplitButton, SplitButtonAction, SplitButtonMenuTrigger } from "@opencode-ai/ui/split-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useLanguage } from "@/context/language"
import { type OpenApp, useOpenInApp } from "@/components/session/open-in-app"
@@ -15,13 +14,13 @@ export function OpenInAppV2(props: { directory: () => string }) {
return (
<Show when={props.directory() && state.canOpen()}>
<SplitButtonV2 class="session-review-v2-open-in-app" onPointerDown={(event) => event.stopPropagation()}>
<TooltipV2
<SplitButton class="session-review-v2-open-in-app" onPointerDown={(event) => event.stopPropagation()}>
<Tooltip
placement="bottom"
value={language.t("session.header.open.ariaLabel", { app: state.current().label })}
class="flex items-center"
>
<SplitButtonV2Action
<SplitButtonAction
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => {
event.stopPropagation()
@@ -34,28 +33,28 @@ export function OpenInAppV2(props: { directory: () => string }) {
<Show when={state.opening()} fallback={<AppIcon id={state.current().icon} class="size-[18px]" />}>
<Spinner class="size-3.5" />
</Show>
</SplitButtonV2Action>
</TooltipV2>
<MenuV2
</SplitButtonAction>
</Tooltip>
<Menu
gutter={4}
modal={false}
placement="bottom-end"
open={state.menu.open}
onOpenChange={(open) => state.setMenu("open", open)}
>
<MenuV2.Trigger
as={SplitButtonV2MenuTrigger}
<Menu.Trigger
as={SplitButtonMenuTrigger}
disabled={state.opening()}
aria-label={language.t("session.header.open.menu")}
onPointerDown={(event) => event.stopPropagation()}
>
<IconV2 name="chevron-down" size="small" />
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content class="open-in-app-v2-menu">
<MenuV2.Group>
<MenuV2.GroupLabel>{language.t("session.header.openIn")}</MenuV2.GroupLabel>
<MenuV2.RadioGroup
<Icon name="chevron-down" size="small" />
</Menu.Trigger>
<Menu.Portal>
<Menu.Content class="open-in-app-v2-menu">
<Menu.Group>
<Menu.GroupLabel>{language.t("session.header.openIn")}</Menu.GroupLabel>
<Menu.RadioGroup
value={state.current().id}
onChange={(value) => {
state.selectApp(value as OpenApp)
@@ -63,7 +62,7 @@ export function OpenInAppV2(props: { directory: () => string }) {
>
<For each={state.options()}>
{(option) => (
<MenuV2.RadioItem
<Menu.RadioItem
value={option.id}
disabled={state.opening()}
onSelect={() => {
@@ -74,13 +73,13 @@ export function OpenInAppV2(props: { directory: () => string }) {
>
<AppIcon id={option.icon} />
{option.label}
</MenuV2.RadioItem>
</Menu.RadioItem>
)}
</For>
</MenuV2.RadioGroup>
</MenuV2.Group>
<MenuV2.Separator />
<MenuV2.Item
</Menu.RadioGroup>
</Menu.Group>
<Menu.Separator />
<Menu.Item
onSelect={() => {
state.setMenu("open", false)
state.copyPath()
@@ -88,11 +87,11 @@ export function OpenInAppV2(props: { directory: () => string }) {
>
<Icon name="copy" size="small" class="text-icon-weak" />
{language.t("session.header.open.copyPath")}
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</SplitButtonV2>
</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
</SplitButton>
</Show>
)
}
@@ -1,4 +1,3 @@
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { createMemo, Show } from "solid-js"
import { createMediaQuery } from "@solid-primitives/media"
import { Portal } from "solid-js/web"
@@ -7,10 +6,10 @@ import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { useSessionLayout } from "@/pages/session/session-layout"
import { StatusPopoverV2 } from "../status-popover"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { reviewTooltipKeybind } from "../command-tooltip-keybind"
import { useTitlebarRightMount } from "../titlebar"
@@ -60,24 +59,24 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
return (
<div class="flex items-center gap-2">
<Show when={props.state.statusVisible}>
<Tooltip placement="bottom" value={props.state.statusLabel}>
<Tooltip appearance="standard" placement="bottom" value={props.state.statusLabel}>
<StatusPopoverV2 />
</Tooltip>
</Show>
<Show when={props.state.reviewVisible}>
<TooltipV2
<Tooltip
class="shrink-0"
placement="bottom"
value={
<>
{props.state.reviewLabel}
<Show when={props.state.reviewKeybind.length > 0}>
<KeybindV2 keys={props.state.reviewKeybind} variant="neutral" />
<Keybind keys={props.state.reviewKeybind} variant="neutral" />
</Show>
</>
}
>
<IconButtonV2
<IconButton
type="button"
variant="ghost-muted"
size="large"
@@ -87,9 +86,9 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
aria-label={props.state.reviewLabel}
aria-expanded={props.state.reviewOpened}
aria-controls="review-panel"
icon={<IconV2 name="sidebar-right" />}
icon={<Icon name="sidebar-right" />}
/>
</TooltipV2>
</Tooltip>
</Show>
</div>
)
@@ -1,9 +1,8 @@
import { createMemo, Show } from "solid-js"
import type { JSX } from "solid-js"
import { useSortable } from "@dnd-kit/solid/sortable"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Tabs } from "@opencode-ai/ui/tabs"
import { useFile } from "@/context/file"
import { useLanguage } from "@/context/language"
@@ -40,31 +39,29 @@ export function SortableTabV2(props: {
<div class="relative">
<Tabs.Trigger
value={props.tab}
onMiddleClick={() => props.onTabClose(props.tab)}
onDblClick={() => props.onTabDoubleClick?.(props.tab)}
closeButton={
<TooltipV2
<Tooltip
value={
<>
{language.t("common.closeTab")}
<Show when={closeTabKeybind().length > 0}>
<KeybindV2 keys={closeTabKeybind()} variant="neutral" />
<Keybind keys={closeTabKeybind()} variant="neutral" />
</Show>
</>
}
placement="bottom"
gutter={10}
>
<IconButton
icon="close-small"
variant="ghost"
<Tabs.CloseButton
class="h-5 w-5"
onClick={() => props.onTabClose(props.tab)}
aria-label={language.t("common.closeTab")}
/>
</TooltipV2>
</Tooltip>
}
hideCloseButton
onMiddleClick={() => props.onTabClose(props.tab)}
onDblClick={() => props.onTabDoubleClick?.(props.tab)}
>
<Show when={content()}>{(value) => value()}</Show>
</Tabs.Trigger>
@@ -2,9 +2,8 @@ import type { JSX } from "solid-js"
import { Show, createEffect, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { useSortable } from "@dnd-kit/solid/sortable"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tabs } from "@opencode-ai/ui/tabs"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { Menu } from "@opencode-ai/ui/menu"
import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title"
import { useTerminal, type LocalPTY } from "@/context/terminal"
import { useLanguage } from "@/context/language"
@@ -116,8 +115,8 @@ export function SortableTerminalTabV2(props: { terminal: LocalPTY; index: number
return (
<div ref={sortable.ref} class="h-full flex items-center outline-none focus:outline-none focus-visible:outline-none">
<MenuV2.Context>
<MenuV2.Context.Trigger class="relative" as="div">
<Menu.Context>
<Menu.Context.Trigger class="relative" as="div">
<Tabs.Trigger
value={props.terminal.id}
onMouseDown={(e) => {
@@ -131,20 +130,11 @@ export function SortableTerminalTabV2(props: { terminal: LocalPTY; index: number
if (e.detail > 0) return
focus()
}}
onMiddleClick={close}
closeButton={
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={(e) => {
e.stopPropagation()
close()
}}
aria-label={language.t("terminal.close")}
/>
<Tabs.CloseButton class="h-5 w-5" onClick={close} aria-label={language.t("terminal.close")} />
}
hideCloseButton
onMiddleClick={close}
>
<span
class="truncate"
@@ -169,9 +159,9 @@ export function SortableTerminalTabV2(props: { terminal: LocalPTY; index: number
/>
</div>
</Show>
</MenuV2.Context.Trigger>
<MenuV2.Context.Portal>
<MenuV2.Context.Content
</Menu.Context.Trigger>
<Menu.Context.Portal>
<Menu.Context.Content
onCloseAutoFocus={(e) => {
if (!editRequested) return
e.preventDefault()
@@ -179,11 +169,11 @@ export function SortableTerminalTabV2(props: { terminal: LocalPTY; index: number
requestAnimationFrame(() => edit())
}}
>
<MenuV2.Item onSelect={() => (editRequested = true)}>{language.t("common.rename")}</MenuV2.Item>
<MenuV2.Item onSelect={close}>{language.t("common.close")}</MenuV2.Item>
</MenuV2.Context.Content>
</MenuV2.Context.Portal>
</MenuV2.Context>
<Menu.Item onSelect={() => (editRequested = true)}>{language.t("common.rename")}</Menu.Item>
<Menu.Item onSelect={close}>{language.t("common.close")}</Menu.Item>
</Menu.Context.Content>
</Menu.Context.Portal>
</Menu.Context>
</div>
)
}
@@ -1,9 +1,9 @@
import { For, Show, createMemo, lazy, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { Button } from "@opencode-ai/ui/button"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TextInput } from "@opencode-ai/ui/text-input"
import { showToast } from "@/utils/toast"
import fuzzysort from "fuzzysort"
import { DEFAULT_PALETTE_KEYBIND, formatKeybind, parseKeybind, useCommand } from "@/context/command"
@@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { SettingsListV2 } from "./settings-v2/parts/list"
const IconV2 = lazy(() => import("@opencode-ai/ui/v2/icon").then((module) => ({ default: module.Icon })))
const Icon = lazy(() => import("@opencode-ai/ui/icon").then((module) => ({ default: module.Icon })))
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
const PALETTE_ID = "command.palette"
@@ -387,12 +387,12 @@ function SettingsKeybindsV2View(props: {
<h2 class="settings-v2-tab-title">{language.t("settings.shortcuts.title")}</h2>
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.shortcuts.description")}</span>
</div>
<ButtonV2 variant="ghost" onClick={props.onReset} disabled={!props.hasOverrides}>
<Button variant="ghost" onClick={props.onReset} disabled={!props.hasOverrides}>
{language.t("settings.shortcuts.reset.button")}
</ButtonV2>
</Button>
</div>
<div class="settings-v2-tab-search">
<TextInputV2
<TextInput
type="search"
appearance="base"
value={store.filter}
@@ -405,12 +405,12 @@ function SettingsKeybindsV2View(props: {
aria-label={language.t("settings.shortcuts.search.placeholder")}
/>
<Show when={store.filter}>
<IconButtonV2
<IconButton
type="button"
variant="ghost-muted"
size="small"
class="settings-v2-tab-search-clear"
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
icon={<Icon name="close" size="large" class="text-v2-icon-icon-muted" />}
onClick={() => setStore("filter", "")}
/>
</Show>
@@ -1,6 +1,6 @@
import { Component, createMemo } from "solid-js"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { Select } from "@opencode-ai/ui/select"
import { TextInput } from "@opencode-ai/ui/text-input"
import { useLanguage } from "@/context/language"
import { ExternalLink } from "../external-link"
import { SettingsListV2 } from "./parts/list"
@@ -42,7 +42,7 @@ const FontSetting: Component<{
return (
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
<div class="w-full sm:w-[220px]">
<TextInputV2
<TextInput
data-action={config().action}
type="text"
appearance="base"
@@ -83,8 +83,7 @@ export const SettingsAppearanceV2: Component = () => {
title={language.t("settings.general.row.colorScheme.title")}
description={language.t("settings.general.row.colorScheme.description")}
>
<SelectV2
appearance="inline"
<Select
data-action="settings-color-scheme"
options={schemeOptions}
current={schemeOptions.find((option) => option === appearance.scheme.current())}
@@ -110,8 +109,7 @@ export const SettingsAppearanceV2: Component = () => {
</>
}
>
<SelectV2
appearance="inline"
<Select
data-action="settings-theme"
options={appearance.theme.options()}
current={appearance.theme.current()}
@@ -1,7 +1,7 @@
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { Button } from "@opencode-ai/ui/button"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { Divider } from "@opencode-ai/ui/divider"
import { TextInput } from "@opencode-ai/ui/text-input"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useMutation } from "@tanstack/solid-query"
import { type Component, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
@@ -69,12 +69,12 @@ export const DialogServerV2: Component<{
<DialogHeader hideClose={true}>
<DialogTitle>{title()}</DialogTitle>
</DialogHeader>
<DividerV2 />
<Divider />
<DialogBody class="flex w-full min-w-0 flex-1 flex-col px-4 pt-4 pb-2">
<div class="flex w-full min-w-0 flex-col gap-6">
<div class="flex w-full min-w-0 flex-col gap-2">
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.url")}</label>
<TextInputV2
<TextInput
type="text"
appearance="large"
class="!w-full self-stretch"
@@ -92,7 +92,7 @@ export const DialogServerV2: Component<{
</div>
<div class="flex w-full min-w-0 flex-col gap-2">
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.name")}</label>
<TextInputV2
<TextInput
type="text"
appearance="large"
class="!w-full self-stretch"
@@ -106,7 +106,7 @@ export const DialogServerV2: Component<{
<div class="grid w-full min-w-0 grid-cols-2 gap-4">
<div class="flex min-w-0 flex-col gap-2">
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.username")}</label>
<TextInputV2
<TextInput
type="text"
appearance="large"
class="!w-full self-stretch"
@@ -119,7 +119,7 @@ export const DialogServerV2: Component<{
</div>
<div class="flex min-w-0 flex-col gap-2">
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.password")}</label>
<TextInputV2
<TextInput
type="password"
appearance="large"
class="!w-full self-stretch"
@@ -134,12 +134,12 @@ export const DialogServerV2: Component<{
</div>
</DialogBody>
<DialogFooter>
<ButtonV2 variant="neutral" disabled={form.state.busy()} onClick={() => dialog.close()}>
<Button variant="neutral" disabled={form.state.busy()} onClick={() => dialog.close()}>
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2 variant="contrast" disabled={form.state.busy()} onClick={form.submit}>
</Button>
<Button variant="contrast" disabled={form.state.busy()} onClick={form.submit}>
{submitLabel()}
</ButtonV2>
</Button>
</DialogFooter>
</Dialog>
)
@@ -1,6 +1,6 @@
import { Component, createEffect, createMemo, createSignal, startTransition } from "solid-js"
import { Dialog } from "@opencode-ai/ui/v2/dialog-v2"
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Icon } from "@opencode-ai/ui/icon"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
@@ -73,66 +73,66 @@ export const DialogSettings: Component<{
return (
<Dialog size="x-large" variant="settings" class="settings-v2-dialog">
<TabsV2
<Tabs
orientation="vertical"
variant="settings"
value={tab()}
onChange={(value) => void startTransition(() => setTab(value))}
class="settings-v2"
>
<TabsV2.List>
<Tabs.List>
<div class="flex flex-col justify-between h-full w-full">
<div class="flex flex-col gap-4 w-full">
{/* Group 1: Preferences */}
<div class="flex flex-col gap-1 w-full">
<TabsV2.Trigger value="general">
<Tabs.Trigger value="general">
<Icon name="sliders" />
{language.t("settings.tab.preferences")}
</TabsV2.Trigger>
<TabsV2.Trigger value="appearance">
</Tabs.Trigger>
<Tabs.Trigger value="appearance">
<Icon name="appearance" />
{language.t("settings.general.section.appearance")}
</TabsV2.Trigger>
<TabsV2.Trigger value="notifications">
</Tabs.Trigger>
<Tabs.Trigger value="notifications">
<Icon name="notifications" />
{language.t("settings.tab.notifications")}
</TabsV2.Trigger>
<TabsV2.Trigger value="shortcuts">
</Tabs.Trigger>
<Tabs.Trigger value="shortcuts">
<Icon name="keyboard" />
{language.t("settings.tab.shortcuts")}
</TabsV2.Trigger>
</Tabs.Trigger>
</div>
{/* Group 2: Environment & Workspaces */}
<div class="flex flex-col gap-1 w-full">
<TabsV2.Trigger value="servers">
<Tabs.Trigger value="servers">
<Icon name="server" />
{language.t("status.popover.tab.servers")}
</TabsV2.Trigger>
<TabsV2.Trigger value="projects">
</Tabs.Trigger>
<Tabs.Trigger value="projects">
<Icon name="folder" />
{language.t("settings.tab.projects")}
</TabsV2.Trigger>
<TabsV2.Trigger value="workspaces">
</Tabs.Trigger>
<Tabs.Trigger value="workspaces">
<Icon name="workspace-isolated" />
{language.t("settings.tab.workspaces")}
</TabsV2.Trigger>
</Tabs.Trigger>
</div>
{/* Group 3: Capabilities & Extensions */}
<div class="flex flex-col gap-1 w-full">
<TabsV2.Trigger value="providers">
<Tabs.Trigger value="providers">
<Icon name="providers" />
{language.t("settings.providers.title")}
</TabsV2.Trigger>
<TabsV2.Trigger value="models">
</Tabs.Trigger>
<Tabs.Trigger value="models">
<Icon name="models" />
{language.t("settings.models.title")}
</TabsV2.Trigger>
<TabsV2.Trigger value="extensions">
</Tabs.Trigger>
<Tabs.Trigger value="extensions">
<Icon name="extensions" />
{language.t("settings.tab.extensions")}
</TabsV2.Trigger>
</Tabs.Trigger>
</div>
</div>
@@ -141,41 +141,41 @@ export const DialogSettings: Component<{
<span>v{platform.version}</span>
</div>
</div>
</TabsV2.List>
</Tabs.List>
<TabsV2.Content value="general" class="settings-v2-panel">
<Tabs.Content value="general" class="settings-v2-panel">
<SettingsGeneral server={server()} sessionID={props.sessionID} />
</TabsV2.Content>
<TabsV2.Content value="appearance" class="settings-v2-panel">
</Tabs.Content>
<Tabs.Content value="appearance" class="settings-v2-panel">
<SettingsAppearanceV2 />
</TabsV2.Content>
<TabsV2.Content value="notifications" class="settings-v2-panel">
</Tabs.Content>
<Tabs.Content value="notifications" class="settings-v2-panel">
<SettingsNotificationsV2 />
</TabsV2.Content>
<TabsV2.Content value="shortcuts" class="settings-v2-panel">
</Tabs.Content>
<Tabs.Content value="shortcuts" class="settings-v2-panel">
<SettingsKeybinds />
</TabsV2.Content>
<TabsV2.Content value="servers" class="settings-v2-panel">
</Tabs.Content>
<Tabs.Content value="servers" class="settings-v2-panel">
<SettingsServersV2 />
</TabsV2.Content>
<TabsV2.Content value="projects" class="settings-v2-panel">
</Tabs.Content>
<Tabs.Content value="projects" class="settings-v2-panel">
<SettingsProjectsV2 />
</TabsV2.Content>
</Tabs.Content>
<SettingsServerScope directory={directory()}>
<TabsV2.Content value="workspaces" class="settings-v2-panel">
<Tabs.Content value="workspaces" class="settings-v2-panel">
<SettingsWorkspacesV2 activeDirectory={directory()} />
</TabsV2.Content>
<TabsV2.Content value="providers" class="settings-v2-panel">
</Tabs.Content>
<Tabs.Content value="providers" class="settings-v2-panel">
<SettingsProvidersV2 directory={directory()} onBack={showProviders} />
</TabsV2.Content>
<TabsV2.Content value="models" class="settings-v2-panel">
</Tabs.Content>
<Tabs.Content value="models" class="settings-v2-panel">
<SettingsModelsV2 />
</TabsV2.Content>
<TabsV2.Content value="extensions" class="settings-v2-panel">
</Tabs.Content>
<Tabs.Content value="extensions" class="settings-v2-panel">
<SettingsExtensionsV2 />
</TabsV2.Content>
</Tabs.Content>
</SettingsServerScope>
</TabsV2>
</Tabs>
</Dialog>
)
}
@@ -1,7 +1,7 @@
import { Component, For, createEffect, createMemo, createResource } from "solid-js"
import { Icon } from "@opencode-ai/ui/icon"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
import { Switch } from "@opencode-ai/ui/switch"
import { Tabs } from "@opencode-ai/ui/tabs"
import { useLanguage } from "@/context/language"
import { useData } from "@/context/server"
import { useServerSDK } from "@/context/server-sdk"
@@ -68,14 +68,14 @@ export const SettingsExtensionsV2: Component = () => {
</div>
<div class="settings-v2-tab-body">
<TabsV2 variant="pill" defaultValue="mcps" class="settings-v2-extensions-tabs">
<TabsV2.List>
<TabsV2.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</TabsV2.Trigger>
<TabsV2.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</TabsV2.Trigger>
<TabsV2.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</TabsV2.Trigger>
</TabsV2.List>
<Tabs variant="pill" defaultValue="mcps" class="settings-v2-extensions-tabs">
<Tabs.List>
<Tabs.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</Tabs.Trigger>
<Tabs.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</Tabs.Trigger>
<Tabs.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</Tabs.Trigger>
</Tabs.List>
<TabsV2.Content value="mcps">
<Tabs.Content value="mcps">
<div class="settings-v2-section">
<div class="flex items-center justify-between">
<span class="text-13-medium text-v2-text-text-base">
@@ -99,9 +99,9 @@ export const SettingsExtensionsV2: Component = () => {
</For>
</div>
</div>
</TabsV2.Content>
</Tabs.Content>
<TabsV2.Content value="plugins">
<Tabs.Content value="plugins">
<div class="settings-v2-section">
<div class="flex items-center justify-between">
<span class="text-13-medium text-v2-text-text-base">
@@ -122,9 +122,9 @@ export const SettingsExtensionsV2: Component = () => {
</For>
</div>
</div>
</TabsV2.Content>
</Tabs.Content>
<TabsV2.Content value="skills">
<Tabs.Content value="skills">
<div class="settings-v2-section">
<div class="flex items-center justify-between">
<span class="text-13-medium text-v2-text-text-base">
@@ -150,8 +150,8 @@ export const SettingsExtensionsV2: Component = () => {
</For>
</div>
</div>
</TabsV2.Content>
</TabsV2>
</Tabs.Content>
</Tabs>
</div>
</>
)
@@ -1,9 +1,9 @@
import { Component, Show, createMemo, createResource } from "solid-js"
import { createMediaQuery } from "@solid-primitives/media"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { Button } from "@opencode-ai/ui/button"
import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
import { TextInput } from "@opencode-ai/ui/text-input"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useUpdaterAction } from "../updater-action"
@@ -100,8 +100,7 @@ const WorkspaceDestinationSetting: Component = () => {
title={language.t("settings.workspaces.default.title")}
description={language.t("settings.workspaces.default.description")}
>
<SelectV2
appearance="inline"
<Select
options={options()}
current={options().find((option) => option.value === settings.workspaces.defaultDestination())}
value={(option) => option.value}
@@ -127,8 +126,7 @@ const ShellSetting: Component<{ controller: ShellSettingsController }> = (props)
title={language.t("settings.general.row.shell.title")}
description={language.t("settings.general.row.shell.description")}
>
<SelectV2
appearance="inline"
<Select
data-action="settings-shell"
options={options()}
current={options().find((option) => option.value === props.controller.current()) ?? options()[0]}
@@ -156,8 +154,7 @@ const AppearanceSection: Component<{ controller: AppearanceSettingsController }>
title={language.t("settings.general.row.colorScheme.title")}
description={language.t("settings.general.row.colorScheme.description")}
>
<SelectV2
appearance="inline"
<Select
data-action="settings-color-scheme"
options={schemeOptions}
current={schemeOptions.find((option) => option === props.controller.scheme.current())}
@@ -183,8 +180,7 @@ const AppearanceSection: Component<{ controller: AppearanceSettingsController }>
</>
}
>
<SelectV2
appearance="inline"
<Select
data-action="settings-theme"
options={props.controller.theme.options()}
current={props.controller.theme.current()}
@@ -213,7 +209,7 @@ const FontSetting: Component<{
return (
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
<div class="w-full sm:w-[220px]">
<TextInputV2
<TextInput
data-action={config().action}
type="text"
appearance="base"
@@ -254,8 +250,7 @@ const SoundSetting: Component<{
const config = () => soundSettings[props.kind]
return (
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
<SelectV2
appearance="inline"
<Select
data-action={config().action}
options={soundOptions}
current={props.channel.current()}
@@ -283,8 +278,7 @@ const LanguageSetting = () => {
title={language.t("settings.general.row.language.title")}
description={language.t("settings.general.row.language.description")}
>
<SelectV2
appearance="inline"
<Select
data-action="settings-language"
options={options()}
placement="bottom-end"
@@ -500,9 +494,9 @@ export const SettingsGeneral: Component<{
title={language.t("settings.updates.row.check.title")}
description={language.t("settings.updates.row.check.description")}
>
<ButtonV2 size="normal" variant="neutral" disabled={!updater.action().run} onClick={() => updater.run()}>
<Button size="normal" variant="neutral" disabled={!updater.action().run} onClick={() => updater.run()}>
{language.t(updater.action().label)}
</ButtonV2>
</Button>
</SettingsRowV2>
</SettingsListV2>
</div>
@@ -1,9 +1,9 @@
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { Switch } from "@opencode-ai/ui/switch"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TextInput } from "@opencode-ai/ui/text-input"
import { type Component, For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useLanguage } from "@/context/language"
@@ -62,7 +62,7 @@ export const SettingsModelsV2: Component = () => {
<InlineServerSelect />
</div>
<div class="settings-v2-tab-search">
<TextInputV2
<TextInput
type="search"
appearance="base"
value={list.filter()}
@@ -75,12 +75,12 @@ export const SettingsModelsV2: Component = () => {
aria-label={language.t("dialog.model.search.placeholder")}
/>
<Show when={list.filter()}>
<IconButtonV2
<IconButton
type="button"
variant="ghost-muted"
size="small"
class="settings-v2-tab-search-clear"
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
icon={<Icon name="close" size="large" class="text-v2-icon-icon-muted" />}
onClick={() => list.clear()}
/>
</Show>
@@ -1,6 +1,6 @@
import { Component } from "solid-js"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { SettingsListV2 } from "./parts/list"
@@ -34,8 +34,7 @@ const SoundSetting: Component<{
const config = () => soundSettings[props.kind]
return (
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
<SelectV2
appearance="inline"
<Select
data-action={config().action}
options={soundOptions}
current={props.channel.current()}
@@ -1,5 +1,5 @@
import { Show, createMemo, type Component } from "solid-js"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { Select } from "@opencode-ai/ui/select"
import { useGlobal } from "@/context/global"
import { ServerConnection, serverName } from "@/context/servers"
@@ -20,8 +20,7 @@ export const InlineServerSelect: Component<{
return (
<Show when={options().length > 1}>
<SelectV2
appearance="inline"
<Select
data-action="settings-server-select"
options={options()}
current={current()}
@@ -1,7 +1,7 @@
import { Component, For, Show, createMemo, createSignal } from "solid-js"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLanguage } from "@/context/language"
import { useGlobal } from "@/context/global"
@@ -51,11 +51,11 @@ export const SettingsProjectsV2: Component = () => {
<span class="text-13-medium text-v2-text-text-base truncate">{name()}</span>
</div>
<div class="flex items-center gap-2 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<IconButtonV2
<IconButton
type="button"
variant="ghost-muted"
size="small"
icon={<IconV2 name="settings-gear" size="small" class="text-v2-icon-icon-muted" />}
icon={<Icon name="settings-gear" size="small" class="text-v2-icon-icon-muted" />}
onClick={(event: MouseEvent) => {
event.stopPropagation()
openProjectSettings(props.project, props.server)
@@ -1,5 +1,5 @@
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
import { Button } from "@opencode-ai/ui/button"
import { Badge } from "@opencode-ai/ui/badge"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { showToast } from "@/utils/toast"
@@ -160,7 +160,7 @@ export const SettingsProvidersV2: Component<{
/>
<div class="settings-v2-provider-main">
<span class="settings-v2-provider-name truncate">{item.name}</span>
<Tag>{type(item)}</Tag>
<Badge>{type(item)}</Badge>
</div>
</div>
<Show
@@ -171,9 +171,9 @@ export const SettingsProvidersV2: Component<{
</span>
}
>
<ButtonV2 size="normal" variant="ghost-muted" onClick={() => void disconnect(item.id, item.name)}>
<Button size="normal" variant="ghost-muted" onClick={() => void disconnect(item.id, item.name)}>
{language.t("common.disconnect")}
</ButtonV2>
</Button>
</Show>
</div>
)}
@@ -199,7 +199,7 @@ export const SettingsProvidersV2: Component<{
<div class="settings-v2-provider-main">
<span class="settings-v2-provider-name">{item.name}</span>
<Show when={item.id === "opencode" || item.id === "opencode-go"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
<Badge>{language.t("dialog.provider.tag.recommended")}</Badge>
</Show>
</div>
<Show when={note(item.id)}>
@@ -207,9 +207,9 @@ export const SettingsProvidersV2: Component<{
</Show>
</div>
</div>
<ButtonV2 size="normal" variant="neutral" icon="plus" onClick={() => connect(item.id)}>
<Button size="normal" variant="neutral" icon="plus" onClick={() => connect(item.id)}>
{language.t("common.connect")}
</ButtonV2>
</Button>
</div>
)}
</For>
@@ -226,14 +226,14 @@ export const SettingsProvidersV2: Component<{
<div class="settings-v2-provider-copy">
<div class="settings-v2-provider-main">
<span class="settings-v2-provider-name">{language.t("provider.custom.title")}</span>
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
<Badge>{language.t("settings.providers.tag.custom")}</Badge>
</div>
<p class="settings-v2-provider-description">
{language.t("settings.providers.custom.description")}
</p>
</div>
</div>
<ButtonV2
<Button
size="normal"
variant="neutral"
icon="plus"
@@ -246,7 +246,7 @@ export const SettingsProvidersV2: Component<{
}}
>
{language.t("common.connect")}
</ButtonV2>
</Button>
</div>
</Show>
</SettingsListV2>
@@ -1,7 +1,7 @@
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { Badge } from "@opencode-ai/ui/badge"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TextInput } from "@opencode-ai/ui/text-input"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import fuzzysort from "fuzzysort"
import { type Component, For, Show, createMemo } from "solid-js"
@@ -61,7 +61,7 @@ export const SettingsServersV2: Component = () => {
</div>
<Show when={showSearch()}>
<div class="settings-v2-tab-search">
<TextInputV2
<TextInput
type="search"
appearance="base"
value={store.filter}
@@ -74,12 +74,12 @@ export const SettingsServersV2: Component = () => {
aria-label={language.t("dialog.server.search.placeholder")}
/>
<Show when={store.filter}>
<IconButtonV2
<IconButton
type="button"
variant="ghost-muted"
size="small"
class="settings-v2-tab-search-clear"
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
icon={<Icon name="close" size="large" class="text-v2-icon-icon-muted" />}
onClick={() => setStore("filter", "")}
/>
</Show>
@@ -126,7 +126,7 @@ export const SettingsServersV2: Component = () => {
</div>
<div class="settings-v2-servers-actions">
<Show when={controller.defaults.available() && isDefault()}>
<Tag>{language.t("dialog.server.status.default")}</Tag>
<Badge>{language.t("dialog.server.status.default")}</Badge>
</Show>
<ServerRowMenu server={item} domain={controller} onEdit={openEdit} />
</div>
@@ -1,5 +1,5 @@
@import "@opencode-ai/ui/v2/text-input-v2.css";
@import "@opencode-ai/ui/v2/button-v2.css";
@import "@opencode-ai/ui/text-input.css";
@import "@opencode-ai/ui/button.css";
[data-component="tabs-v2"][data-variant="settings"] {
height: 100%;
@@ -3,12 +3,12 @@ import { For, Show, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import type { SessionInfo } from "@opencode-ai/client/promise"
import { useQuery } from "@tanstack/solid-query"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Dialog, DialogFooter, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/v2/dialog-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Button } from "@opencode-ai/ui/button"
import { Dialog, DialogFooter, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Menu } from "@opencode-ai/ui/menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { getFilename } from "@opencode-ai/util/path"
import { useLanguage } from "@/context/language"
@@ -273,33 +273,33 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr
</span>
<div class="settings-v2-workspaces-toolbar-actions">
<Show when={projects().length > 1}>
<MenuV2 placement="bottom-end" gutter={6}>
<MenuV2.Trigger class="flex h-6 max-w-48 items-center gap-1 rounded-sm px-2 text-13-medium hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed">
<Menu placement="bottom-end" gutter={6}>
<Menu.Trigger class="flex h-6 max-w-48 items-center gap-1 rounded-sm px-2 text-13-medium hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed">
<span class="min-w-0 truncate">
{projectOptions().find((option) => option.id === selectedProject())?.label}
</span>
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content>
</Menu.Trigger>
<Menu.Portal>
<Menu.Content>
<For each={projectOptions()}>
{(option) => (
<MenuV2.Item onSelect={() => setStore("project", option.id)}>
<Menu.Item onSelect={() => setStore("project", option.id)}>
<span class="min-w-0 flex-1 truncate">{option.label}</span>
<Show when={selectedProject() === option.id}>
<Icon name="check" size="small" class="shrink-0" />
</Show>
</MenuV2.Item>
</Menu.Item>
)}
</For>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</Menu.Content>
</Menu.Portal>
</Menu>
</Show>
<Show when={filtered().length > 0}>
<MenuV2 placement="bottom-end" gutter={4}>
<MenuV2.Trigger
as={IconButtonV2}
<Menu placement="bottom-end" gutter={4}>
<Menu.Trigger
as={IconButton}
type="button"
variant="ghost-muted"
size="small"
@@ -307,16 +307,16 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr
disabled={!!store.transaction}
icon={<Icon name="outline-dots" size="small" />}
/>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Item onSelect={confirmDeleteAll}>
<Menu.Portal>
<Menu.Content>
<Menu.Item onSelect={confirmDeleteAll}>
<span class="settings-v2-workspaces-delete-all">
{language.t("settings.workspaces.deleteAll")}
</span>
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
</Show>
</div>
</div>
@@ -335,7 +335,7 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr
<div class="settings-v2-workspaces-row-header">
<div class="settings-v2-workspaces-copy">
<div class="settings-v2-workspaces-main">
<TooltipV2
<Tooltip
value={workspace.directory}
placement="top-start"
contentClass="max-w-[calc(100vw-32px)] break-all"
@@ -348,24 +348,21 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr
>
{workspace.directory}
</span>
</TooltipV2>
</Tooltip>
</div>
<span class="settings-v2-workspaces-meta">{sessionCount(workspace)}</span>
</div>
<div class="settings-v2-workspaces-row-actions">
<Show when={lastActive(workspace)}>
{(value) => (
<TooltipV2
value={language.t("settings.workspaces.lastActiveSession")}
placement="top-end"
>
<Tooltip value={language.t("settings.workspaces.lastActiveSession")} placement="top-end">
<span tabIndex={0} class="settings-v2-workspaces-active">
{value()}
</span>
</TooltipV2>
</Tooltip>
)}
</Show>
<IconButtonV2
<IconButton
type="button"
variant="ghost-muted"
size="small"
@@ -428,12 +425,12 @@ function DialogDeleteAllWorkspaces(props: { count: number; project: string; onDe
/>
</DialogHeader>
<DialogFooter>
<ButtonV2 type="button" variant="neutral" onClick={() => dialog.close()}>
<Button type="button" variant="neutral" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2 type="button" variant="danger" onClick={remove}>
</Button>
<Button type="button" variant="danger" onClick={remove}>
{language.t("settings.workspaces.deleteAll")}
</ButtonV2>
</Button>
</DialogFooter>
</Dialog>
)
@@ -486,17 +483,17 @@ function DialogDeleteWorkspace(props: {
/>
</DialogHeader>
<DialogFooter>
<ButtonV2 type="button" variant="neutral" onClick={() => dialog.close()}>
<Button type="button" variant="neutral" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2
</Button>
<Button
type="button"
variant="danger"
disabled={status.isPending || status.isError || status.data?.result.active}
onClick={remove}
>
{language.t("workspace.delete.button")}
</ButtonV2>
</Button>
</DialogFooter>
</Dialog>
)
@@ -48,10 +48,9 @@ export function StatusPopoverBody(props: { shown: boolean }) {
<Tabs
aria-label={language.t("status.popover.ariaLabel")}
class="tabs bg-background-strong rounded-xl overflow-hidden"
data-component="tabs"
data-active="mcp"
defaultValue="mcp"
variant="alt"
variant="underline"
>
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
@@ -111,6 +110,7 @@ export function StatusPopoverBody(props: { shown: boolean }) {
</span>
<div onClick={(event) => event.stopPropagation()}>
<Switch
appearance="standard"
checked={enabled()}
disabled={toggleMcp.isPending && toggleMcp.variables === name}
onChange={() => {
@@ -1,7 +1,6 @@
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Popover } from "@opencode-ai/ui/popover"
import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
import { useLanguage } from "@/context/language"
@@ -157,7 +156,7 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
<Popover
open={props.state.shown}
onOpenChange={props.state.onOpenChange}
triggerAs={IconButtonV2}
triggerAs={IconButton}
triggerProps={{
variant: "ghost-muted",
size: "large",
@@ -167,7 +166,7 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
}}
trigger={
<div class="relative size-4">
<IconV2 name={props.state.shown ? "status-active" : "status"} />
<Icon name={props.state.shown ? "status-active" : "status"} />
<div
class={`absolute -top-1 -right-1 size-2 rounded-full border border-[var(--v2-background-bg-deep)] ${serverStatusDotClass(props.state)}`}
/>
@@ -2,8 +2,8 @@ import { createEffect, createMemo, createSignal, onCleanup, Show, type Ref } fro
import { makeEventListener } from "@solid-primitives/event-listener"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { createMutation } from "@tanstack/solid-query"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { useGlobal, useServerCtx } from "@/context/global"
import { useLanguage } from "@/context/language"
import { ServerConnection, serverName, useServers } from "@/context/servers"
@@ -281,7 +281,7 @@ export function TabNavItem(props: {
</a>
<div data-slot="tab-close">
<IconButtonV2
<IconButton
size="small"
variant="ghost-muted"
class="hover-reveal relative z-10 group-hover:opacity-100 group-data-[active=true]:opacity-100 group-data-[editing=true]:opacity-100"
@@ -290,7 +290,7 @@ export function TabNavItem(props: {
event.stopPropagation()
}}
onClick={closeTab}
icon={<IconV2 name="xmark-small" />}
icon={<Icon name="xmark-small" />}
/>
</div>
</div>
@@ -377,7 +377,7 @@ export function DraftTabItem(props: {
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base [-webkit-user-drag:none]"
>
<span class="flex size-4 shrink-0 items-center justify-center">
<IconV2 name="edit" />
<Icon name="edit" />
</span>
<span
data-titlebar-tab-title
@@ -387,7 +387,7 @@ export function DraftTabItem(props: {
</span>
</a>
<div data-slot="tab-close">
<IconButtonV2
<IconButton
size="small"
variant="ghost-muted"
onPointerDown={(event) => {
@@ -400,7 +400,7 @@ export function DraftTabItem(props: {
}}
class="hover-reveal relative z-10 group-hover:opacity-100 group-data-[active=true]:opacity-100 group-data-[editing=true]:opacity-100"
onClick={closeTab}
icon={<IconV2 name="xmark-small" />}
icon={<Icon name="xmark-small" />}
aria-label={language.t("common.closeTab")}
/>
</div>
+14 -14
View File
@@ -12,10 +12,10 @@ import {
} from "solid-js"
import { createStore } from "solid-js/store"
import { useLocation, useNavigate } from "@solidjs/router"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { LayoutRoute, useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform"
@@ -330,28 +330,28 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
<Show when={windows() || linux()}>
<WindowsAppMenu command={command} platform={platform} />
</Show>
<TooltipV2
<Tooltip
placement="bottom"
value={
<>
{language.t("home.title")}
<KeybindV2 keys={command.keybindParts("home.toggle")} variant="neutral" />
<Keybind keys={command.keybindParts("home.toggle")} variant="neutral" />
</>
}
class="shrink-0"
>
<IconButtonV2
<IconButton
type="button"
variant="ghost-muted"
size="large"
class="!w-9 shrink-0"
icon={<IconV2 name="grid-plus" />}
icon={<Icon name="grid-plus" />}
state={layout.route().type === "home" ? "pressed" : undefined}
onClick={toggleHome}
aria-label={language.t("home.title")}
aria-pressed={layout.route().type === "home"}
/>
</TooltipV2>
</Tooltip>
<TitlebarTabStrip
tabs={tabsStore}
@@ -368,25 +368,25 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
}}
onReorder={(keys) => tabsStoreActions.reorder(keys)}
/>
<TooltipV2
<Tooltip
placement="bottom"
value={
<>
{language.t("command.session.new")}
<KeybindV2 keys={newTabTooltipKeybind(command)} variant="neutral" />
<Keybind keys={newTabTooltipKeybind(command)} variant="neutral" />
</>
}
>
<IconButtonV2
<IconButton
type="button"
variant="ghost-muted"
size="large"
class="shrink-0"
icon={<IconV2 name="plus" />}
icon={<Icon name="plus" />}
onClick={openNewTab}
aria-label={language.t("command.session.new")}
/>
</TooltipV2>
</Tooltip>
<div class="flex-1" />
<TitlebarV2Right state={v2RightState()} />
</div>
@@ -1,8 +1,7 @@
import { For, Show, type JSX } from "solid-js"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { For, type JSX } from "solid-js"
import { Menu } from "@opencode-ai/ui/menu"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { useCommand } from "@/context/command"
import { DESKTOP_MENU, desktopMenuVisible, type DesktopMenuAction, type DesktopMenuEntry } from "@/desktop-menu"
@@ -47,32 +46,32 @@ export function WindowsAppMenu(props: {
}
return (
<DropdownMenu gutter={4} modal={false} placement="bottom-start">
<Menu appearance="standard" gutter={4} modal={false} placement="bottom-start">
<div
data-component="desktop-icon-button"
class="flex h-7 w-9 shrink-0 items-center justify-center rounded-[6px] px-1"
>
<DropdownMenu.Trigger
as={IconButtonV2}
<Menu.Trigger
as={IconButton}
variant="ghost-muted"
size="large"
icon={<IconV2 name="menu" />}
icon={<Icon name="menu" />}
aria-label={language.t("desktop.menu.ariaLabel")}
onPointerDown={rememberFocus}
onKeyDown={rememberFocus}
/>
</div>
<DropdownMenu.Portal>
<DropdownMenu.Content class="desktop-app-menu">
<DropdownMenu.Group>
<DropdownMenu.GroupLabel class="desktop-app-menu-heading">OpenCode</DropdownMenu.GroupLabel>
<Menu.Portal>
<Menu.Content class="desktop-app-menu">
<Menu.Group>
<Menu.GroupLabel class="desktop-app-menu-heading">OpenCode</Menu.GroupLabel>
<For each={DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows"))}>
{(menu) => (
<DesktopMenuSubmenu label={language.t(menu.labelKey)}>
<For each={menu.items?.filter((entry) => desktopMenuVisible(entry, "windows"))}>
{(entry) => {
// Static menu data: an early return keeps the union narrowing a Show fallback would lose.
if (entry.type === "separator") return <DropdownMenu.Separator />
if (entry.type === "separator") return <Menu.Separator />
return (
<DesktopMenuItem
label={entry.labelKey ? language.t(entry.labelKey) : ""}
@@ -86,36 +85,28 @@ export function WindowsAppMenu(props: {
</DesktopMenuSubmenu>
)}
</For>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</Menu.Group>
</Menu.Content>
</Menu.Portal>
</Menu>
)
}
function DesktopMenuSubmenu(props: { label: string; children: JSX.Element }) {
return (
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>
<span data-slot="dropdown-menu-item-label">{props.label}</span>
<span data-slot="desktop-app-menu-chevron">
<Icon name="chevron-right" size="small" />
</span>
</DropdownMenu.SubTrigger>
<DropdownMenu.Portal>
<DropdownMenu.SubContent class="desktop-app-menu">{props.children}</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>
<Menu.Sub>
<Menu.SubTrigger>{props.label}</Menu.SubTrigger>
<Menu.Portal>
<Menu.SubContent class="desktop-app-menu desktop-app-menu-sub">{props.children}</Menu.SubContent>
</Menu.Portal>
</Menu.Sub>
)
}
function DesktopMenuItem(props: { label: string; keybind?: string; disabled?: boolean; onSelect: () => void }) {
return (
<DropdownMenu.Item disabled={props.disabled} onSelect={props.onSelect}>
<DropdownMenu.ItemLabel>{props.label}</DropdownMenu.ItemLabel>
<Show when={props.keybind}>
<span data-slot="desktop-app-menu-keybind">{props.keybind}</span>
</Show>
</DropdownMenu.Item>
<Menu.Item disabled={props.disabled} onSelect={props.onSelect} shortcut={props.keybind}>
{props.label}
</Menu.Item>
)
}
+1 -1
View File
@@ -11,7 +11,7 @@ import { decode64 } from "@/utils/base64"
import { same } from "@/utils/same"
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
import { createPathHelpers } from "./file/path"
import type { ProjectAvatarVariant } from "@opencode-ai/ui/v2/project-avatar-v2"
import type { ProjectAvatarVariant } from "@opencode-ai/ui/project-avatar"
import { SessionStateKey } from "@/utils/server-scope"
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
import { requireServerKey } from "@/utils/session-route"
+10 -21
View File
@@ -1,6 +1,6 @@
@import "@opencode-ai/ui/styles/tailwind";
@import "@opencode-ai/session-ui/styles";
@import "@opencode-ai/ui/v2/styles/tailwind.css";
@import "@opencode-ai/ui/styles/tokens";
@import "tw-animate-css";
@font-face {
@@ -30,23 +30,22 @@
container-name: getting-started;
}
[data-component="dropdown-menu-content"].desktop-app-menu,
[data-component="dropdown-menu-sub-content"].desktop-app-menu {
[data-component="menu-v2-content"].desktop-app-menu {
min-width: 160px;
padding: 2px;
}
[data-component="dropdown-menu-content"].desktop-app-menu {
[data-component="menu-v2-content"].desktop-app-menu:not(.desktop-app-menu-sub) {
width: 160px;
}
[data-component="dropdown-menu-sub-content"].desktop-app-menu {
[data-component="menu-v2-content"].desktop-app-menu-sub {
width: max-content;
min-width: 240px;
max-width: min(320px, calc(100vw - 24px));
}
[data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-group-label"] {
[data-component="menu-v2-content"].desktop-app-menu [data-slot="menu-v2-group-label"] {
display: flex;
align-items: center;
height: 28px;
@@ -57,10 +56,7 @@
color: var(--text-weak);
}
[data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-item"],
[data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-sub-trigger"],
[data-component="dropdown-menu-sub-content"].desktop-app-menu [data-slot="dropdown-menu-item"],
[data-component="dropdown-menu-sub-content"].desktop-app-menu [data-slot="dropdown-menu-sub-trigger"] {
[data-component="menu-v2-content"].desktop-app-menu [data-component="menu-v2-item"] {
min-height: 28px;
padding: 0 12px;
gap: 8px;
@@ -68,12 +64,11 @@
line-height: 1;
}
[data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-item-label"],
[data-component="dropdown-menu-sub-content"].desktop-app-menu [data-slot="dropdown-menu-item-label"] {
[data-component="menu-v2-content"].desktop-app-menu [data-slot="menu-v2-item-content"] {
white-space: nowrap;
}
[data-slot="desktop-app-menu-keybind"] {
[data-component="menu-v2-content"].desktop-app-menu [data-slot="menu-v2-item-shortcut"] {
margin-left: auto;
color: var(--text-weak);
font-size: var(--font-size-x-small);
@@ -81,19 +76,13 @@
white-space: nowrap;
}
[data-slot="desktop-app-menu-chevron"] {
display: flex;
margin-left: auto;
color: var(--icon-base);
}
[data-component="getting-started-actions"] {
display: flex;
flex-direction: column;
gap: 0.75rem; /* gap-3 */
}
[data-component="getting-started-actions"] > [data-component="button"] {
[data-component="getting-started-actions"] > [data-component="button-v2"] {
width: 100%;
}
@@ -103,7 +92,7 @@
align-items: center;
}
[data-component="getting-started-actions"] > [data-component="button"] {
[data-component="getting-started-actions"] > [data-component="button-v2"] {
width: auto;
}
}
@@ -6,11 +6,11 @@ import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/d
import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers"
import { RestrictToElement } from "@dnd-kit/dom/modifiers"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Menu } from "@opencode-ai/ui/menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { getProjectAvatarVariant, type HomeProjectSelection, type LocalProject } from "@/context/layout"
import { ServerConnection } from "@/context/servers"
import { useLanguage } from "@/context/language"
@@ -82,18 +82,18 @@ export function HomeProjectsView(props: HomeProjectsViewProps) {
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
<div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
<Show when={props.servers.length === 1 && !(props.projects.length === 0 && props.recentlyClosed.length > 0)}>
<TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
<IconButtonV2
<Tooltip placement="bottom" value={props.language.t("home.project.add")}>
<IconButton
data-action="home-add-project"
variant="ghost-muted"
size="large"
class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
icon={<IconV2 name="folder-add-left" />}
icon={<Icon name="folder-add-left" />}
disabled={props.serverHealth(props.servers[0])?.healthy === false}
onClick={() => props.onChooseProject(props.servers[0])}
aria-label={props.language.t("home.project.add")}
/>
</TooltipV2>
</Tooltip>
</Show>
</div>
<ScrollView data-slot="home-projects-scroll" class="min-h-0 min-w-0 shrink">
@@ -161,7 +161,7 @@ export function HomeUtilityNav(props: {
class="text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted"
onClick={props.onOpenSettings}
>
<IconV2 name="settings-gear" size="small" />
<Icon name="settings-gear" size="small" />
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("sidebar.settings")}</span>
</HomeProjectNavButton>
<HomeProjectNavButton
@@ -169,7 +169,7 @@ export function HomeUtilityNav(props: {
class="text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted"
onClick={props.onOpenHelp}
>
<IconV2 name="help" size="small" />
<Icon name="help" size="small" />
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("sidebar.help")}</span>
</HomeProjectNavButton>
</div>
@@ -234,7 +234,7 @@ function HomeServerRow(props: {
}}
onPointerDown={(event) => event.preventDefault()}
>
<IconV2
<Icon
name="chevron-down"
size="small"
class="transition-transform duration-150 ease-in-out"
@@ -280,17 +280,17 @@ function HomeServerRow(props: {
open={props.contextMenuOpen(contextMenuID())}
onOpenChange={(open) => props.onSetContextMenuOpen(contextMenuID(), open)}
/>
<TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("home.project.add")}>
<IconButtonV2
<Tooltip class="flex shrink-0 items-center" placement="bottom" value={props.language.t("home.project.add")}>
<IconButton
data-action="home-add-project"
variant="ghost-muted"
size="small"
icon={<IconV2 name="folder-add-left" />}
icon={<Icon name="folder-add-left" />}
aria-label={props.language.t("home.project.add")}
disabled={props.health?.healthy === false}
onClick={() => props.onChooseProject(props.server)}
/>
</TooltipV2>
</Tooltip>
</div>
</div>
)
@@ -394,7 +394,7 @@ function HomeProjectEmpty(
disabled={unreachable()}
onClick={() => props.onChooseProject(props.server)}
>
<IconV2 name="folder-add-left" size="small" />
<Icon name="folder-add-left" size="small" />
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("home.project.add")}</span>
</HomeProjectNavButton>
<Show when={props.items.length > 0}>
@@ -423,7 +423,7 @@ function HomeRecentlyClosedRow(
return worktree
}
return (
<TooltipV2 placement="right" value={path()}>
<Tooltip placement="right" value={path()}>
<HomeProjectNavButton
type="button"
data-component="home-recently-closed-row"
@@ -434,7 +434,7 @@ function HomeRecentlyClosedRow(
<HomeProjectAvatar project={props.project} outline />
<span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span>
</HomeProjectNavButton>
</TooltipV2>
</Tooltip>
)
}
@@ -523,55 +523,55 @@ function HomeProjectRow(
`}
data-menu={props.contextMenuOpen(contextMenuID())}
>
<MenuV2
<Menu
gutter={6}
modal={false}
placement="bottom-end"
open={props.contextMenuOpen(contextMenuID())}
onOpenChange={(open) => props.onSetContextMenuOpen(contextMenuID(), open)}
>
<MenuV2.Trigger
as={IconButtonV2}
<Menu.Trigger
as={IconButton}
data-action="home-project-menu"
variant="ghost-muted"
size="small"
icon={<IconV2 name="outline-dots" />}
icon={<Icon name="outline-dots" />}
aria-label={props.language.t("common.moreOptions")}
/>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Item onSelect={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}>
<Menu.Portal>
<Menu.Content>
<Menu.Item onSelect={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}>
{props.language.t("command.session.new")}
</MenuV2.Item>
<MenuV2.Item onSelect={() => props.onEditProject(props.server, props.project)}>
</Menu.Item>
<Menu.Item onSelect={() => props.onEditProject(props.server, props.project)}>
{props.language.t("dialog.project.edit.title")}
</MenuV2.Item>
</Menu.Item>
<Show when={props.canRevealProject(props.server)}>
<MenuV2.Item onSelect={() => props.onRevealProject(props.server, props.project)}>
<Menu.Item onSelect={() => props.onRevealProject(props.server, props.project)}>
{props.language.t(
fileManagerApp(platform.platform === "desktop" ? (platform.os ?? "unknown") : "unknown")
.actionLabel,
)}
</MenuV2.Item>
</Menu.Item>
</Show>
<MenuV2.Item
<Menu.Item
disabled={props.unseen === 0}
onSelect={() => props.onClearNotifications(props.server, props.project)}
>
{props.language.t("sidebar.project.clearNotifications")}
</MenuV2.Item>
<MenuV2.Separator />
<MenuV2.Item onSelect={() => props.onCloseProject(props.server, props.project.worktree)}>
</Menu.Item>
<Menu.Separator />
<Menu.Item onSelect={() => props.onCloseProject(props.server, props.project.worktree)}>
{props.language.t("common.close")}
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
<IconButtonV2
</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
<IconButton
data-action="home-project-new-session"
variant="ghost-muted"
size="small"
icon={<IconV2 name="edit" />}
icon={<Icon name="edit" />}
aria-label={props.language.t("command.session.new")}
onClick={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}
/>
@@ -2,10 +2,10 @@ import type { SessionInfo } from "@opencode-ai/client/promise"
import { createMemo, For, Show, Suspense } from "solid-js"
import { Spinner } from "@opencode-ai/ui/spinner"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
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 { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useLanguage } from "@/context/language"
import { ServerConnection } from "@/context/servers"
import { SessionTabAvatarView } from "@/pages/layout/session-tab-avatar"
@@ -83,7 +83,7 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
<Suspense>
<Show when={props.groups.length > 0 && props.canCreateSession}>
<div class="pointer-events-none absolute right-0 top-[84px] z-20 flex lg:top-[108px]">
<ButtonV2
<Button
data-action="home-new-session"
variant="ghost-muted"
size="normal"
@@ -92,7 +92,7 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
onClick={props.onCreateSession}
>
{props.language.t("command.session.new")}
</ButtonV2>
</Button>
</div>
</Show>
</Suspense>
@@ -273,7 +273,7 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
duration-[120ms] ease-in-out hover:bg-v2-background-bg-layer-02 focus-within:bg-v2-background-bg-layer-02
`}
>
<IconV2 name="magnifying-glass" />
<Icon name="magnifying-glass" />
<input
ref={props.onSetSearchInput}
class={`
@@ -317,12 +317,12 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
}}
/>
<Show when={props.searchValue}>
<IconButtonV2
<IconButton
type="button"
variant="ghost-muted"
size="small"
class="relative z-20 shrink-0"
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
icon={<Icon name="close" size="large" class="text-v2-icon-icon-muted" />}
aria-label={props.searchPlaceholder}
onClick={() => {
props.onSearchClose()
@@ -458,12 +458,12 @@ function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionReco
group-hover/session:opacity-100 focus-within:opacity-100
`}
>
<TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("common.archive")}>
<IconButtonV2
<Tooltip class="flex shrink-0 items-center" placement="bottom" value={props.language.t("common.archive")}>
<IconButton
data-action="home-session-archive"
variant="ghost-muted"
size="large"
icon={<IconV2 name="archive" />}
icon={<Icon name="archive" />}
aria-label={props.language.t("common.archive")}
onClick={(event) => {
event.preventDefault()
@@ -471,7 +471,7 @@ function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionReco
void props.onArchiveSession(props.record.session)
}}
/>
</TooltipV2>
</Tooltip>
</div>
</Show>
</div>
@@ -525,9 +525,9 @@ function HomeSessionsEmpty(props: { onNewSession?: () => void; language: ReturnT
</p>
<Show when={props.onNewSession}>
{(onNewSession) => (
<ButtonV2 data-action="home-new-session" variant="neutral" size="normal" icon="edit" onClick={onNewSession()}>
<Button data-action="home-new-session" variant="neutral" size="normal" icon="edit" onClick={onNewSession()}>
{props.language.t("command.session.new")}
</ButtonV2>
</Button>
)}
</Show>
</div>
@@ -1,9 +1,8 @@
import type { LocalProject } from "@/context/layout"
import { getProjectAvatarVariant } from "@/context/layout"
import { getProjectAvatarVariant, type LocalProject } from "@/context/layout"
import type { ServerConnection } from "@/context/servers"
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
import { SessionProgressIndicatorV2 } from "@opencode-ai/session-ui/v2/session-progress-indicator-v2"
import { Show } from "solid-js"
@@ -1,8 +1,7 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { WordmarkV2 } from "@opencode-ai/ui/v2/wordmark-v2"
import { Icon } from "@opencode-ai/ui/icon"
import { Wordmark } from "@opencode-ai/ui/wordmark"
import { Show, createMemo, createSignal } from "solid-js"
import { createStore } from "solid-js/store"
import { Portal } from "solid-js/web"
@@ -47,7 +46,7 @@ export function NewSessionView(props: {
>
<div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6">
<div class={NEW_SESSION_CONTENT_WIDTH}>
<WordmarkV2 class="h-auto w-full text-v2-background-bg-inverse" />
<Wordmark class="h-auto w-full text-v2-background-bg-inverse" />
<div class="mt-8 flex flex-col gap-8">
<PromptInputV2Composer controller={props.input} accentSubmit={props.workspace.selection.workspace()} />
<Show when={props.project.empty()}>
@@ -96,7 +95,7 @@ export function NewSessionStatus(props: { mount: HTMLElement | null; visible: bo
{(mount) => (
<Portal mount={mount}>
<Show when={props.visible}>
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
<Tooltip appearance="standard" placement="bottom" value={language.t("status.popover.trigger")}>
<StatusPopoverV2 />
</Tooltip>
</Show>
@@ -153,7 +152,7 @@ function ProviderTip() {
<Icon name="chevron-down" size="small" class="-rotate-90" />
</span>
</button>
<TooltipV2
<Tooltip
class="hover-reveal absolute left-full top-0 flex h-6 w-7 items-center justify-end delay-0 duration-0 group-hover/provider-tip:delay-[250ms] group-hover/provider-tip:duration-150 group-hover/provider-tip:opacity-100 focus-within:delay-0 focus-within:duration-0 focus-within:opacity-100"
placement="top"
openDelay={1000}
@@ -167,7 +166,7 @@ function ProviderTip() {
>
<Icon name="xmark-small" />
</button>
</TooltipV2>
</Tooltip>
</div>
</div>
</Show>
@@ -21,8 +21,8 @@ export function useNewSessionCommands(input: {
title: language.t("command.palette"),
hidden: true,
onSelect: async () => {
const { DialogSelectFile } = await import("@/components/dialog-select-file")
void dialog.show(() => <DialogSelectFile />)
const { DialogCommandPaletteV2 } = await import("@/components/dialog-command-palette-v2")
void dialog.show(() => <DialogCommandPaletteV2 />)
},
},
{
+6 -26
View File
@@ -30,10 +30,9 @@ import { createStore } from "solid-js/store"
import type { SessionReviewLineComment } from "@opencode-ai/session-ui/session-review"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Select } from "@opencode-ai/ui/select"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { isScrollKeyTarget, scrollKey, scrollKeyOwner } from "@opencode-ai/ui/scroll-view"
import { Tabs } from "@opencode-ai/ui/tabs"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Button } from "@opencode-ai/ui/button"
import { createAutoScroll } from "@opencode-ai/ui/hooks"
import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
import { showToast } from "@/utils/toast"
@@ -195,9 +194,9 @@ function SessionErrorFallback(props: { error: unknown; sessionID?: string; serve
</div>
)}
</Show>
<ButtonV2 variant="neutral" size="normal" icon="xmark-small" onClick={closeTab}>
<Button variant="neutral" size="normal" icon="xmark-small" onClick={closeTab}>
{language.t("session.error.notFound.closeTab")}
</ButtonV2>
</Button>
</div>
</div>
)
@@ -1039,25 +1038,6 @@ export default function Page() {
return (
<Select
options={changesOptions()}
current={reviewMode()}
label={changesLabel}
onSelect={(option) => option && controller.layout.view().review.setMode(option)}
variant="ghost"
size="small"
valueClass="text-14-medium"
/>
)
}
const changesTitleV2 = () => {
if (!canReview()) {
return null
}
return (
<SelectV2
appearance="inline"
options={changesOptions()}
current={reviewMode()}
label={changesLabel}
@@ -1161,7 +1141,7 @@ export default function Page() {
// updates such as session switches.
const reviewPanelV2Props = () => ({
get title() {
return changesTitleV2()
return changesTitle()
},
get empty() {
return reviewEmptyV2()
@@ -1752,22 +1732,22 @@ export default function Page() {
>
<Tabs.Trigger
value="session"
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
classList={{
"!w-1/2 !max-w-none": true,
"!border-b-0 !border-t !border-border-weak-base [&:has([data-selected])]:!border-t-transparent": bottom,
}}
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
onClick={() => setStore("mobileTab", "session")}
>
{language.t("session.tab.session")}
</Tabs.Trigger>
<Tabs.Trigger
value="changes"
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
classList={{
"!w-1/2 !max-w-none !border-r-0": true,
"!border-b-0 !border-t !border-border-weak-base [&:has([data-selected])]:!border-t-transparent": bottom,
}}
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
onClick={() => setStore("mobileTab", "changes")}
>
{hasReview()
@@ -1,6 +1,6 @@
import { useLanguage } from "@/context/language"
import { useCommand } from "@/context/command"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { Keybind } from "@opencode-ai/ui/keybind"
import { For, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { SessionComposerPullout } from "./session-composer-pullout"
@@ -52,7 +52,7 @@ export function SessionBackgroundDock(props: {
<span>
<span class="text-v2-text-text-muted">{moving()}</span>
<span class="pl-2">
<KeybindV2 keys={command.keybindParts("session.background")} variant="neutral" />
<Keybind keys={command.keybindParts("session.background")} variant="neutral" />
</span>
</span>
)}
@@ -1,4 +1,5 @@
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { TextReveal } from "@opencode-ai/ui/text-reveal"
import { createResizeObserver } from "@solid-primitives/resize-observer"
@@ -107,7 +108,7 @@ export function SessionComposerPullout(props: {
<IconButton
data-action={`session-${props.name}-toggle-button`}
data-collapsed={props.collapsed ? "true" : "false"}
icon="chevron-down"
icon={<Icon name="chevron-down" />}
size="normal"
variant="ghost"
style={{ transform: `rotate(${value() * 180}deg)` }}
@@ -3,6 +3,7 @@ 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 { Icon } from "@opencode-ai/ui/icon"
import { useLanguage } from "@/context/language"
export function SessionFollowupDock(props: {
@@ -48,7 +49,7 @@ export function SessionFollowupDock(props: {
<div class="ml-auto shrink-0">
<IconButton
data-collapsed={store.collapsed ? "true" : "false"}
icon="chevron-down"
icon={<Icon name="chevron-down" />}
size="normal"
variant="ghost"
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
@@ -79,7 +80,7 @@ export function SessionFollowupDock(props: {
<span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
<Button
size="small"
variant="secondary"
variant="neutral"
class="shrink-0"
disabled={!!props.sending}
onClick={() => props.onSend(item.id)}
@@ -38,14 +38,14 @@ export function SessionPermissionDock(props: {
{language.t("ui.permission.deny")}
</Button>
<Button
variant="secondary"
variant="neutral"
size="normal"
onClick={() => props.onDecide("always")}
disabled={props.responding}
>
{language.t("ui.permission.allowAlways")}
</Button>
<Button variant="primary" size="normal" onClick={() => props.onDecide("once")} disabled={props.responding}>
<Button variant="contrast" size="normal" onClick={() => props.onDecide("once")} disabled={props.responding}>
{language.t("ui.permission.allowOnce")}
</Button>
</div>
@@ -2,6 +2,7 @@ import { For, Show, createEffect, createMemo, onCleanup, onMount, type Component
import { createStore } from "solid-js/store"
import { useMutation } from "@tanstack/solid-query"
import { Button } from "@opencode-ai/ui/button"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
import { Icon } from "@opencode-ai/ui/icon"
import { useSpring } from "@opencode-ai/ui/motion-spring"
@@ -499,19 +500,14 @@ export const SessionQuestionDock: Component<{ request: FormInfo; onSubmit: () =>
</For>
</div>
</Show>
<button
type="button"
data-component="icon-button"
data-icon="chevron-down"
data-size="normal"
data-variant="ghost"
<IconButton
icon={<Icon name="chevron-down" size="small" />}
variant="ghost"
disabled={sending()}
style={{ transform: `rotate(${hidden() * 180}deg)` }}
onClick={store.minimized ? restore : minimize}
aria-label={language.t(store.minimized ? "session.question.restore" : "session.question.minimize")}
>
<Icon name="chevron-down" size="small" />
</button>
/>
</div>
</>
}
@@ -522,12 +518,12 @@ export const SessionQuestionDock: Component<{ request: FormInfo; onSubmit: () =>
</Button>
<div data-slot="question-footer-actions">
<Show when={store.tab > 0}>
<Button variant="secondary" size="large" disabled={sending()} onClick={back}>
<Button variant="neutral" size="large" disabled={sending()} onClick={back}>
{language.t("ui.common.back")}
</Button>
</Show>
<Button
variant={last() ? "primary" : "secondary"}
variant={last() ? "contrast" : "neutral"}
size="large"
disabled={sending()}
onClick={next}
@@ -1,8 +1,8 @@
import { For, Show, createEffect, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
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 { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { useLanguage } from "@/context/language"
export function SessionRevertDock(props: {
@@ -45,7 +45,7 @@ export function SessionRevertDock(props: {
onClick={toggle}
onKeyDown={onHeaderKeyDown}
>
<IconV2 name="outline-reset" size="normal" class="text-v2-icon-icon-muted" />
<Icon 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,
@@ -61,8 +61,8 @@ export function SessionRevertDock(props: {
</span>
</Show>
<div class="ml-auto shrink-0">
<IconButtonV2
icon={<IconV2 name="outline-chevron-down" size="small" />}
<IconButton
icon={<Icon name="outline-chevron-down" size="small" />}
size="large"
variant="ghost-muted"
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
@@ -95,7 +95,7 @@ export function SessionRevertDock(props: {
<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
<Button
size="small"
variant="neutral"
class="shrink-0"
@@ -103,7 +103,7 @@ export function SessionRevertDock(props: {
onClick={() => props.onRestore(item.id)}
>
{language.t("session.revertDock.restore")}
</ButtonV2>
</Button>
</div>
)}
</For>
+15 -15
View File
@@ -7,8 +7,8 @@ import { useFileComponent } from "@opencode-ai/ui/context/file"
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
import { createLineCommentControllerV2 } from "@opencode-ai/session-ui/v2/line-comment-annotations-v2"
import { sampledChecksum } from "@opencode-ai/util/encode"
import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { LineCommentOverflowIcon } from "@opencode-ai/ui/line-comment"
import { Menu } from "@opencode-ai/ui/menu"
import { Tabs } from "@opencode-ai/ui/tabs"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { showToast } from "@/utils/toast"
@@ -26,7 +26,7 @@ type SessionFileViewProps = {
const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions"
function FileCommentMenuV2(props: {
function FileCommentMenu(props: {
moreLabel: string
editLabel: string
deleteLabel: string
@@ -35,17 +35,17 @@ function FileCommentMenuV2(props: {
}) {
return (
<div onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
<MenuV2 gutter={4}>
<MenuV2.Trigger as="button" type="button" data-slot="line-comment-v2-overflow" aria-label={props.moreLabel}>
<LineCommentV2OverflowIcon />
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Item onSelect={props.onEdit}>{props.editLabel}</MenuV2.Item>
<MenuV2.Item onSelect={props.onDelete}>{props.deleteLabel}</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
<Menu gutter={4}>
<Menu.Trigger as="button" type="button" data-slot="line-comment-v2-overflow" aria-label={props.moreLabel}>
<LineCommentOverflowIcon />
</Menu.Trigger>
<Menu.Portal>
<Menu.Content>
<Menu.Item onSelect={props.onEdit}>{props.editLabel}</Menu.Item>
<Menu.Item onSelect={props.onDelete}>{props.deleteLabel}</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
</div>
)
}
@@ -331,7 +331,7 @@ export function SessionFileView(props: SessionFileViewProps) {
},
editSubmitLabel: language.t("common.save"),
renderCommentActions: (_, controls) => (
<FileCommentMenuV2
<FileCommentMenu
moreLabel={language.t("common.moreOptions")}
editLabel={language.t("common.edit")}
deleteLabel={language.t("common.delete")}
@@ -10,9 +10,8 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Mark } from "@opencode-ai/ui/logo"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import FileTree from "@/components/file-tree"
@@ -313,30 +312,27 @@ export function SessionSidePanel(props: {
<Show when={contextOpen()}>
<Tabs.Trigger
value="context"
onMiddleClick={() => tabs().close("context")}
closeButton={
<TooltipV2
<Tooltip
value={
<>
{language.t("common.closeTab")}
<Show when={closeTabKeybind().length > 0}>
<KeybindV2 keys={closeTabKeybind()} variant="neutral" />
<Keybind keys={closeTabKeybind()} variant="neutral" />
</Show>
</>
}
placement="bottom"
gutter={10}
>
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
<Tabs.CloseButton
onClick={() => tabs().close("context")}
aria-label={language.t("common.closeTab")}
/>
</TooltipV2>
</Tooltip>
}
hideCloseButton
onMiddleClick={() => tabs().close("context")}
>
<div class="flex items-center gap-2">
<SessionContextUsage variant="indicator" />
@@ -360,30 +356,27 @@ export function SessionSidePanel(props: {
>
<Tabs.Trigger
value={SESSION_OPEN_FILE_TAB}
onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
closeButton={
<TooltipV2
<Tooltip
value={
<>
{language.t("common.closeTab")}
<Show when={closeTabKeybind().length > 0}>
<KeybindV2 keys={closeTabKeybind()} variant="neutral" />
<Keybind keys={closeTabKeybind()} variant="neutral" />
</Show>
</>
}
placement="bottom"
gutter={10}
>
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
<Tabs.CloseButton
onClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
aria-label={language.t("common.closeTab")}
/>
</TooltipV2>
</Tooltip>
}
hideCloseButton
onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
>
<div class="flex items-center gap-1.5 italic">
<Icon name="open-file" size="small" />
@@ -394,26 +387,26 @@ export function SessionSidePanel(props: {
)}
</For>
<div class="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center bg-v2-background-bg-base">
<TooltipV2
<Tooltip
value={
<>
{language.t("command.file.open")}
<Show when={openFileKeybind().length > 0}>
<KeybindV2 keys={openFileKeybind()} variant="neutral" />
<Keybind keys={openFileKeybind()} variant="neutral" />
</Show>
</>
}
placement="bottom"
class="flex items-center"
>
<IconButtonV2
<IconButton
icon={<Icon name="plus-small" />}
variant="ghost-muted"
size="large"
onClick={() => openFileBrowser()}
aria-label={language.t("command.file.open")}
/>
</TooltipV2>
</Tooltip>
</div>
</Tabs.List>
<div
@@ -503,7 +496,7 @@ export function SessionSidePanel(props: {
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
>
<Tabs
variant="pill"
variant="surface"
value={fileTreeTab()}
onChange={setFileTreeTabValue}
class="h-full"
@@ -10,8 +10,9 @@ import { RestrictToElement } from "@dnd-kit/dom/modifiers"
import { Tabs } from "@opencode-ai/ui/tabs"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Keybind } from "@opencode-ai/ui/keybind"
import { SortableTerminalTabV2 } from "@/components/session/session-sortable-terminal-tab-v2"
import { Terminal } from "@/components/terminal"
@@ -249,7 +250,7 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
>
<div class="flex flex-col h-full">
<Tabs
variant="normal"
variant="panel"
value={terminal.active()}
onChange={(id) => terminal.open(id)}
class="!h-[52px] !flex-none"
@@ -266,12 +267,12 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
{(pty, index) => <SortableTerminalTabV2 terminal={pty} index={index()} onClose={close} />}
</For>
<div class="h-full flex items-center justify-center">
<TooltipV2
<Tooltip
value={
<>
{language.t("command.terminal.new")}
<Show when={newTerminalKeybind().length > 0}>
<KeybindV2 keys={newTerminalKeybind()} variant="neutral" />
<Keybind keys={newTerminalKeybind()} variant="neutral" />
</Show>
</>
}
@@ -279,13 +280,12 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
class="flex items-center"
>
<IconButton
icon="plus-small"
icon={<Icon name="plus-small" size="large" />}
variant="ghost"
iconSize="large"
onClick={() => terminal.new({ focus: true })}
aria-label={language.t("command.terminal.new")}
/>
</TooltipV2>
</Tooltip>
</div>
</Tabs.List>
</Tabs>
@@ -1,6 +1,6 @@
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
import { DialogFooter, DialogHeader, DialogTitleGroup, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { DialogFooter, DialogHeader, DialogTitleGroup, Dialog } from "@opencode-ai/ui/dialog"
import { Button } from "@opencode-ai/ui/button"
import { useNavigate } from "@solidjs/router"
import { createEffect, createMemo, on } from "solid-js"
import { createStore } from "solid-js/store"
@@ -186,7 +186,7 @@ export function createTimelineController(input: { session: TimelineSessionSource
dialog.close()
}
return (
<DialogV2 fit>
<Dialog fit>
<DialogHeader hideClose>
<DialogTitleGroup
title={language.t("session.delete.title")}
@@ -194,14 +194,14 @@ export function createTimelineController(input: { session: TimelineSessionSource
/>
</DialogHeader>
<DialogFooter>
<ButtonV2 variant="ghost" onClick={() => dialog.close()}>
<Button variant="ghost" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2 variant="danger" onClick={confirm}>
</Button>
<Button variant="danger" onClick={confirm}>
{language.t("session.delete.button")}
</ButtonV2>
</Button>
</DialogFooter>
</DialogV2>
</Dialog>
)
}
@@ -15,13 +15,13 @@ import { createVirtualizer, defaultRangeExtractor, elementScroll, type VirtualIt
import { Card } from "@opencode-ai/ui/card"
import { MessageDivider, SessionShellMessage, type UserActions } from "@opencode-ai/session-ui/message-part"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { InlineInput } from "@opencode-ai/ui/inline-input"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Menu } from "@opencode-ai/ui/menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
import { Button } from "@opencode-ai/ui/button"
import { SessionRetry } from "@opencode-ai/session-ui/session-retry"
import { isScrollKeyTarget, scrollKey, scrollKeyOwner, ScrollView } from "@opencode-ai/ui/scroll-view"
import { TextReveal } from "@opencode-ai/ui/text-reveal"
@@ -138,7 +138,7 @@ function WorkspaceMoveAction(props: {
: "flex h-[46px] w-full items-center gap-2 rounded-b-[6px] px-3 pe-9 pt-2.5 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted focus-visible:outline-none"
}
>
<IconV2 name="workspace-new" class="shrink-0 text-v2-icon-icon-muted" />
<Icon name="workspace-new" class="shrink-0 text-v2-icon-icon-muted" />
<span class="min-w-0 truncate">{language.t("workspace.move.title")}</span>
</SessionWorkspaceMenu>
<button
@@ -154,7 +154,7 @@ function WorkspaceMoveAction(props: {
props.onDismiss()
}}
>
<IconV2 name="xmark-small" />
<Icon name="xmark-small" />
</button>
</div>
)
@@ -203,12 +203,12 @@ function SessionSummaryPanel(props: {
gutter={-22}
class={`${row} hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed`}
>
<IconV2 name={props.local ? "monitor" : "workspace-isolated"} class="shrink-0 text-v2-icon-icon-muted" />
<Icon name={props.local ? "monitor" : "workspace-isolated"} class="shrink-0 text-v2-icon-icon-muted" />
<span class="min-w-0 flex-1 truncate text-start">{location()}</span>
<IconV2 name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</SessionWorkspaceMenu>
<div class={row}>
<IconV2 name="branch" class="shrink-0 text-v2-icon-icon-muted" />
<Icon name="branch" class="shrink-0 text-v2-icon-icon-muted" />
<Show
when={props.branch}
fallback={
@@ -235,13 +235,13 @@ function SessionSummaryPanel(props: {
class={`${row} hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none`}
onClick={props.onReview}
>
<IconV2 name="review" class="shrink-0 text-v2-icon-icon-muted" />
<Icon name="review" class="shrink-0 text-v2-icon-icon-muted" />
<Show when={props.diffs} fallback={<span>{language.t("session.review.loadingChanges")}</span>}>
{(diffs) => (
<Show when={diffs().length > 0} fallback={<span>{language.t("session.review.noChanges")}</span>}>
<span>{language.plural("ui.sessionTurn.diffs.changed", diffs().length)}</span>
<span class="text-v2-text-text-muted">·</span>
<DiffChanges changes={diffs()} />
<DiffChanges appearance="standard" changes={diffs()} />
</Show>
)}
</Show>
@@ -1175,11 +1175,11 @@ function MessageTimelineView(
when={workspaceSession()}
fallback={
<span class="flex size-6 shrink-0 items-center justify-center text-v2-icon-icon-muted">
<IconV2 name="monitor" />
<Icon name="monitor" />
</span>
}
>
<TooltipV2
<Tooltip
placement="bottom-start"
value={sessionDirectory()}
contentClass="max-w-[calc(100vw-32px)] break-all"
@@ -1189,9 +1189,9 @@ function MessageTimelineView(
aria-label={sessionDirectory()}
class="flex size-6 shrink-0 items-center justify-center text-v2-icon-icon-accent"
>
<IconV2 name="workspace-isolated" />
<Icon name="workspace-isolated" />
</span>
</TooltipV2>
</Tooltip>
</Show>
<Show when={parentID()}>
<button
@@ -1268,8 +1268,8 @@ function MessageTimelineView(
onOpenChange={setSummary}
>
<KobaltePopover.Trigger
as={IconButtonV2}
icon={<IconV2 name="window-analytics" />}
as={IconButton}
icon={<Icon name="window-analytics" />}
variant="ghost-muted"
size="large"
state={summaryOpen() ? "pressed" : undefined}
@@ -1300,7 +1300,7 @@ function MessageTimelineView(
)}
</Show>
<Show when={!parentID()}>
<MenuV2
<Menu
gutter={6}
placement="bottom-end"
open={title.menuOpen}
@@ -1309,9 +1309,9 @@ function MessageTimelineView(
if (open) return
}}
>
<MenuV2.Trigger
as={IconButtonV2}
icon={<IconV2 name="outline-dots" />}
<Menu.Trigger
as={IconButton}
icon={<Icon name="outline-dots" />}
variant="ghost-muted"
size="large"
state={share.open || title.pendingShare ? "pressed" : undefined}
@@ -1321,8 +1321,8 @@ function MessageTimelineView(
more = el
}}
/>
<MenuV2.Portal>
<MenuV2.Content
<Menu.Portal>
<Menu.Content
style={{ width: "120px", "min-width": "120px" }}
onCloseAutoFocus={(event) => {
if (title.pendingRename) {
@@ -1340,34 +1340,34 @@ function MessageTimelineView(
}
}}
>
<MenuV2.Item
<Menu.Item
onSelect={() => {
setTitle("pendingRename", true)
setTitle("menuOpen", false)
}}
>
{language.t("common.rename")}
</MenuV2.Item>
</Menu.Item>
<Show when={shareEnabled()}>
<MenuV2.Item
<Menu.Item
onSelect={() => {
setTitle({ pendingShare: true, menuOpen: false })
}}
>
{language.t("session.share.action.share")}...
</MenuV2.Item>
</Menu.Item>
</Show>
<MenuV2.Item onSelect={() => void props.action.export(id)}>
<Menu.Item onSelect={() => void props.action.export(id)}>
{language.t("common.export")}...
</MenuV2.Item>
</Menu.Item>
{/* TODO: Need a V2 session archive API. */}
<MenuV2.Separator />
<MenuV2.Item onSelect={() => props.action.showDelete(id)}>
<Menu.Separator />
<Menu.Item onSelect={() => props.action.showDelete(id)}>
{language.t("common.delete")}...
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
<KobaltePopover
open={share.open}
@@ -1415,7 +1415,7 @@ function MessageTimelineView(
<Show
when={shareUrl()}
fallback={
<ButtonV2
<Button
variant="contrast"
class="w-full"
onClick={() => void props.action.share()}
@@ -1424,7 +1424,7 @@ function MessageTimelineView(
{props.pending.share()
? language.t("session.share.action.publishing")
: language.t("session.share.action.publish")}
</ButtonV2>
</Button>
}
>
<div class="flex flex-col gap-2">
@@ -1441,26 +1441,26 @@ function MessageTimelineView(
>
{shareUrl()}
</div>
<IconButtonV2
<IconButton
type="button"
size="small"
variant="ghost-muted"
icon={<IconV2 name="outline-copy" />}
icon={<Icon name="outline-copy" />}
aria-label={language.t("session.share.copy.copyLink")}
onClick={() => void props.action.copyShareUrl()}
/>
<IconButtonV2
<IconButton
type="button"
size="small"
variant="ghost-muted"
icon={<IconV2 name="outline-square-arrow" />}
icon={<Icon name="outline-square-arrow" />}
aria-label={language.t("session.share.action.view")}
onClick={props.action.viewShare}
disabled={props.pending.unshare()}
/>
</div>
<div class="flex w-full">
<ButtonV2
<Button
variant="outline"
class="w-full"
onClick={() => void props.action.unshare()}
@@ -1469,7 +1469,7 @@ function MessageTimelineView(
{props.pending.unshare()
? language.t("session.share.action.unpublishing")
: language.t("session.share.action.unpublish")}
</ButtonV2>
</Button>
</div>
</div>
</Show>
@@ -62,13 +62,16 @@ test("reports a divergent native offset once and ignores equal offsets and unrel
})
test("keeps checking until stale reset-delay callbacks can no longer win", async () => {
const route = document.createElement("section")
const viewport = document.createElement("div")
const targetWindow = new Window()
const mutations = controlledMutations(targetWindow)
const animation = controlledAnimationFrames(targetWindow)
const route = targetWindow.document.createElement("section")
const viewport = targetWindow.document.createElement("div")
route.append(viewport)
document.body.append(route)
targetWindow.document.body.append(route)
const instance = {
scrollElement: viewport,
targetWindow: window,
targetWindow,
scrollOffset: 79_400,
options: {
horizontal: false,
@@ -83,20 +86,23 @@ test("keeps checking until stale reset-delay callbacks can no longer win", async
instance.scrollOffset = offset
})
route.remove()
document.body.append(route)
await new Promise((resolve) => setTimeout(resolve, 0))
await frames(1)
expect(instance.scrollOffset).toBe(0)
try {
mutations.remove(route)
mutations.append(targetWindow.document.body, route)
animation.run(16)
expect(instance.scrollOffset).toBe(0)
instance.scrollOffset = 79_400
await new Promise((resolve) => setTimeout(resolve, 25))
await frames(3)
instance.scrollOffset = 79_400
animation.run(32)
animation.run(48)
expect(instance.scrollOffset).toBe(0)
expect(calls).toEqual([0, 0])
cleanup?.()
route.remove()
expect(instance.scrollOffset).toBe(0)
expect(calls).toEqual([0, 0])
expect(animation.pending()).toBe(0)
} finally {
cleanup?.()
await targetWindow.happyDOM.close()
}
})
test.each([
@@ -235,3 +241,29 @@ function controlledMutations(targetWindow: Window) {
},
}
}
function controlledAnimationFrames(targetWindow: Window) {
let time = 0
let id = 0
const callbacks = new Map<number, FrameRequestCallback>()
Object.defineProperty(targetWindow.performance, "now", { value: () => time })
Object.defineProperty(targetWindow, "requestAnimationFrame", {
value: (callback: FrameRequestCallback) => {
id += 1
callbacks.set(id, callback)
return id
},
})
Object.defineProperty(targetWindow, "cancelAnimationFrame", {
value: (frame: number) => callbacks.delete(frame),
})
return {
run(at: number) {
time = at
const pending = [...callbacks.values()]
callbacks.clear()
pending.forEach((callback) => callback(at))
},
pending: () => callbacks.size,
}
}
@@ -4,9 +4,8 @@ import type { SessionStatus } from "@opencode-ai/client/promise"
import { onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { useSessionLayout } from "./session-layout"
import { useDialog } from "@opencode-ai/ui/context"
import { useDialog, useI18n } from "@opencode-ai/ui/context"
import { DialogUsageExceeded } from "@/components/dialog-usage-exceeded"
import { useI18n } from "@opencode-ai/ui/context"
const GO_UPSELL_FREE_TIER_LAST_SEEN_AT = "go_upsell_last_seen_at"
const GO_UPSELL_FREE_TIER_DONT_SHOW = "go_upsell_dont_show"
@@ -219,8 +219,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const openFile = () => {
void openDialog(
() => import("@/components/dialog-select-file"),
(x) => dialog.show(() => <x.DialogSelectFile onOpenFile={showAllFiles} />),
() => import("@/components/dialog-command-palette-v2"),
(x) => dialog.show(() => <x.DialogCommandPaletteV2 onOpenFile={showAllFiles} />),
)
}
@@ -7,7 +7,7 @@ import {
SessionReviewV2Sidebar,
} from "@opencode-ai/session-ui/v2/session-review-v2"
import { SessionReviewFilePreviewV2 } from "@opencode-ai/session-ui/v2/session-review-file-preview-v2"
import { DiffChanges } from "@opencode-ai/ui/v2/diff-changes-v2"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import type {
SessionReviewComment,
SessionReviewCommentActions,
@@ -1,5 +1,5 @@
import { FileIcon } from "@opencode-ai/ui/file-icon"
import "@opencode-ai/ui/v2/file-tree-v2.css"
import "@opencode-ai/ui/file-tree.css"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createEffect, createMemo, createSignal, For, Show } from "solid-js"
import { kindChange, kindLabel, type Kind } from "@/components/file-tree-v2"
+14 -9
View File
@@ -1,15 +1,18 @@
import { Icon, type IconProps } from "@opencode-ai/ui/icon"
import type { ToastOptions, ToastVariant } from "@opencode-ai/ui/toast"
import { ToastV2, showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2"
import { Toast, showToast, toaster, type ToastOptions } from "@opencode-ai/ui/toast"
export function ToastRegion() {
return <ToastV2.Region />
type AppToastOptions = Omit<ToastOptions, "icon"> & {
icon?: IconProps["name"]
}
export function showToast(options: ToastOptions | string) {
if (typeof options === "string") return showToastV2(options)
export function ToastRegion() {
return <Toast.Region />
}
return showToastV2({
function showAppToast(options: AppToastOptions | string) {
if (typeof options === "string") return showToast(options)
return showToast({
...options,
icon: resolveIcon(options.icon, options.variant),
actions: options.actions?.map((action) => ({
@@ -19,11 +22,13 @@ export function showToast(options: ToastOptions | string) {
})
}
export { showAppToast as showToast }
export function dismissToast(toastId: number) {
return toasterV2.dismiss(toastId)
return toaster.dismiss(toastId)
}
function resolveIcon(icon: IconProps["name"] | undefined, variant: ToastVariant | undefined) {
function resolveIcon(icon: IconProps["name"] | undefined, variant: ToastOptions["variant"]) {
const name = icon ?? (variant === "success" ? "check" : undefined)
if (!name) return
return <Icon name={name} />
+33 -38
View File
@@ -1,15 +1,15 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { showToast } from "@opencode-ai/ui/toast"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
import { LoaderV2 } from "@opencode-ai/ui/v2/loader-v2"
import { RadioGroupV2, RadioItemV2 } from "@opencode-ai/ui/v2/radio-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { Button } from "@opencode-ai/ui/button"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { Divider } from "@opencode-ai/ui/divider"
import { Loader } from "@opencode-ai/ui/loader"
import { RadioGroup, RadioItem } from "@opencode-ai/ui/radio"
import { TextInput } from "@opencode-ai/ui/text-input"
import { createMemo, For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { showToast } from "@/utils/toast"
import { useWslAddServerProbes } from "./add-server-probes"
import { useWslServers } from "./context"
import { addServerViewModel, type AddServerText } from "./settings-model"
@@ -50,7 +50,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
fallback={<div class="settings-v2-wsl-loading">{controller.loadError()}</div>}
>
<div class="settings-v2-wsl-loading">
<LoaderV2 />
<Loader />
</div>
</Show>
</Dialog>
@@ -73,7 +73,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
>
<Dialog fit class="settings-v2-wsl-dialog">
<div class="settings-v2-wsl-loading">
<LoaderV2 />
<Loader />
</div>
</Dialog>
</Show>
@@ -85,13 +85,13 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
{controller.view() === "main" ? language.t("wsl.server.add") : language.t("wsl.onboarding.installDistro")}
</DialogTitle>
</DialogHeader>
<DividerV2 />
<Divider />
<Show
when={controller.view() === "main"}
fallback={
<>
<DialogBody class="settings-v2-wsl-dialog-body settings-v2-wsl-catalog-picker">
<TextInputV2
<TextInput
class="settings-v2-wsl-catalog-search"
appearance="large"
placeholder={language.t("wsl.onboarding.searchDistros")}
@@ -100,7 +100,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
onInput={(event) => controller.setCatalogSearch(event.currentTarget.value)}
/>
<div class="settings-v2-wsl-catalog-list">
<RadioGroupV2
<RadioGroup
hideLabel
class="settings-v2-wsl-distro-group"
label={language.t("wsl.onboarding.installDistro")}
@@ -110,7 +110,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
>
<For each={model().filteredInstallableDistros}>
{(item) => (
<RadioItemV2
<RadioItem
class="settings-v2-wsl-distro-row settings-v2-wsl-catalog-row"
value={item.name}
disabled={model().busy}
@@ -118,23 +118,23 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
/>
)}
</For>
</RadioGroupV2>
</RadioGroup>
</div>
</DialogBody>
<DialogFooter>
<ButtonV2 variant="neutral" disabled={model().busy} onClick={controller.closeCatalog}>
<Button variant="neutral" disabled={model().busy} onClick={controller.closeCatalog}>
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2
</Button>
<Button
variant={model().installingCatalogDistro ? "loading" : "contrast"}
disabled={!model().installingCatalogDistro && (model().busy || !model().catalogTarget)}
style={{ width: "99px" }}
onClick={controller.installCatalogDistro}
>
<Show when={model().installingCatalogDistro} fallback={language.t("wsl.onboarding.installDistro")}>
<LoaderV2 />
<Loader />
</Show>
</ButtonV2>
</Button>
</DialogFooter>
</>
}
@@ -142,14 +142,9 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
<DialogBody class="settings-v2-wsl-dialog-body">
<div class="settings-v2-wsl-section-header">
<span class="settings-v2-wsl-section-title">{language.t("wsl.onboarding.installedDistros")}</span>
<ButtonV2
variant="ghost-muted"
size="small"
disabled={model().busy}
onClick={controller.refreshDistros}
>
<Button variant="ghost-muted" size="small" disabled={model().busy} onClick={controller.refreshDistros}>
{language.t("wsl.onboarding.checkAgain")}
</ButtonV2>
</Button>
</div>
<Show
@@ -165,7 +160,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
}
>
<div class="settings-v2-wsl-distro-list">
<RadioGroupV2
<RadioGroup
hideLabel
class="settings-v2-wsl-distro-group"
label={language.t("wsl.onboarding.installedDistros")}
@@ -177,7 +172,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
{(item) => {
const status = () => model().distroStatuses[item.name] ?? null
return (
<RadioItemV2
<RadioItem
class={`settings-v2-wsl-distro-row${item.version === 1 ? " settings-v2-wsl-distro-row--unsupported" : ""}`}
value={item.name}
disabled={item.version === 1 || model().busy}
@@ -195,7 +190,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
)
}}
</For>
</RadioGroupV2>
</RadioGroup>
</div>
</Show>
@@ -230,19 +225,19 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
</DialogBody>
<DialogFooter>
<ButtonV2 variant="neutral" disabled={controller.adding()} onClick={controller.close}>
<Button variant="neutral" disabled={controller.adding()} onClick={controller.close}>
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2
</Button>
<Button
variant={primaryButton().loading ? "loading" : primaryButton().variant}
disabled={!primaryButton().loading && primaryButton().disabled}
style={primaryButtonStyle()}
onClick={controller.runPrimary}
>
<Show when={primaryButton().loading} fallback={translate(language, primaryButton().label)}>
<LoaderV2 />
<Loader />
</Show>
</ButtonV2>
</Button>
</DialogFooter>
</Show>
</Dialog>
@@ -439,14 +434,14 @@ function DialogWslSetup(props: {
</Show>
</div>
<Show when={props.state === "unavailable" && props.installable}>
<ButtonV2 variant="neutral" disabled={props.busy} onClick={props.onInstall}>
<Button variant="neutral" disabled={props.busy} onClick={props.onInstall}>
{language.t("wsl.onboarding.installWsl")}
</ButtonV2>
</Button>
</Show>
<Show when={props.state !== "unavailable"}>
<ButtonV2 variant="neutral" onClick={() => dialog.close()}>
<Button variant="neutral" onClick={() => dialog.close()}>
{language.t("common.close")}
</ButtonV2>
</Button>
</Show>
</div>
</Dialog>
+40 -42
View File
@@ -1,9 +1,9 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
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 { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { Badge } from "@opencode-ai/ui/badge"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Menu } from "@opencode-ai/ui/menu"
import { useMutation } from "@tanstack/solid-query"
import fuzzysort from "fuzzysort"
import { type Accessor, For, Show, createMemo } from "solid-js"
@@ -32,22 +32,22 @@ export function AddServerMenu(props: { onAddServer: () => void }) {
<Show
when={platform.wslServers}
fallback={
<ButtonV2 variant="ghost-muted" icon="plus" onClick={props.onAddServer}>
<Button variant="ghost-muted" icon="plus" onClick={props.onAddServer}>
{language.t("dialog.server.add.button")}
</ButtonV2>
</Button>
}
>
<MenuV2 gutter={4} modal={false} placement="bottom-end">
<MenuV2.Trigger as={ButtonV2} variant="ghost-muted" icon="plus">
<Menu gutter={4} modal={false} placement="bottom-end">
<Menu.Trigger as={Button} variant="ghost-muted" icon="plus">
{language.t("dialog.server.add.button")}
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Item onSelect={props.onAddServer}>{language.t("dialog.server.add.button")}</MenuV2.Item>
<MenuV2.Item onSelect={openAddWsl}>{language.t("wsl.server.add")}</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</Menu.Trigger>
<Menu.Portal>
<Menu.Content>
<Menu.Item onSelect={props.onAddServer}>{language.t("dialog.server.add.button")}</Menu.Item>
<Menu.Item onSelect={openAddWsl}>{language.t("wsl.server.add")}</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
</Show>
)
}
@@ -113,54 +113,52 @@ export function WslServerSettings(props: {
</div>
<div class="settings-v2-servers-actions">
<Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
<Tag>{language.t("dialog.server.status.default")}</Tag>
<Badge>{language.t("dialog.server.status.default")}</Badge>
</Show>
<Show when={opencodeAction()}>
{(label) => (
<ButtonV2
<Button
size="small"
disabled={busy() || request.isPending}
onClick={() => api && request.mutate(() => api.installOpencode(item.config.distro))}
>
{busy() ? language.t("wsl.server.updating") : language.t(label())}
</ButtonV2>
</Button>
)}
</Show>
<MenuV2 gutter={4} modal={false} placement="bottom-end">
<MenuV2.Trigger
as={IconButtonV2}
<Menu gutter={4} modal={false} placement="bottom-end">
<Menu.Trigger
as={IconButton}
variant="ghost-muted"
size="small"
icon={<IconV2 name="outline-dots" />}
icon={<Icon name="outline-dots" />}
aria-label={language.t("common.moreOptions")}
/>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Group>
<MenuV2.GroupLabel>{language.t("wsl.server.menu.label")}</MenuV2.GroupLabel>
<Menu.Portal>
<Menu.Content>
<Menu.Group>
<Menu.GroupLabel>{language.t("wsl.server.menu.label")}</Menu.GroupLabel>
<Show when={wslRuntimeRetryable(item.runtime)}>
<MenuV2.Item onSelect={() => api && request.mutate(() => api.startServer(key))}>
<Menu.Item onSelect={() => api && request.mutate(() => api.startServer(key))}>
{language.t("wsl.server.retryStart")}
</MenuV2.Item>
</Menu.Item>
</Show>
<Show when={props.domain.defaults.available() && props.domain.defaults.key() !== key}>
<MenuV2.Item onSelect={() => props.domain.defaults.set(key)}>
<Menu.Item onSelect={() => props.domain.defaults.set(key)}>
{language.t("dialog.server.menu.default")}
</MenuV2.Item>
</Menu.Item>
</Show>
<Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
<MenuV2.Item onSelect={() => props.domain.defaults.set(null)}>
<Menu.Item onSelect={() => props.domain.defaults.set(null)}>
{language.t("dialog.server.menu.defaultRemove")}
</MenuV2.Item>
</Menu.Item>
</Show>
<MenuV2.Separator />
<MenuV2.Item onSelect={() => remove(key)}>
{language.t("dialog.server.menu.delete")}
</MenuV2.Item>
</MenuV2.Group>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
<Menu.Separator />
<Menu.Item onSelect={() => remove(key)}>{language.t("dialog.server.menu.delete")}</Menu.Item>
</Menu.Group>
</Menu.Content>
</Menu.Portal>
</Menu>
</div>
</div>
)
+19 -19
View File
@@ -1,44 +1,44 @@
import { beforeEach, describe, expect, test } from "bun:test"
import { createSignal, type JSX } from "solid-js"
import { showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2"
import { showToast, toaster } from "@opencode-ai/ui/toast"
describe("showToastV2", () => {
describe("showToast", () => {
// The toast registry is module state, so each test starts from an empty stack.
beforeEach(() => {
toasterV2.dismiss()
toaster.dismiss()
})
test("coalesces exact active content", () => {
const first = showToastV2({ title: "Repeated error", description: "Try again" })
const second = showToastV2({ title: "Repeated error", description: "Try again" })
const different = showToastV2({ title: "Repeated error", description: "A different error" })
const first = showToast({ title: "Repeated error", description: "Try again" })
const second = showToast({ title: "Repeated error", description: "Try again" })
const different = showToast({ title: "Repeated error", description: "A different error" })
expect(second).toBe(first)
expect(different).not.toBe(first)
toasterV2.dismiss(first)
toasterV2.dismiss(different)
toaster.dismiss(first)
toaster.dismiss(different)
})
test("allows dismissed content to appear again", () => {
const first = showToastV2("Dismiss and retry")
toasterV2.dismiss(first)
const first = showToast("Dismiss and retry")
toaster.dismiss(first)
const second = showToastV2("Dismiss and retry")
const second = showToast("Dismiss and retry")
expect(second).not.toBe(first)
toasterV2.dismiss(second)
toaster.dismiss(second)
})
test("recreates matching content when it is not the topmost toast", () => {
const first = showToastV2("First toast")
const topmost = showToastV2("Topmost toast")
const repeated = showToastV2("First toast")
const first = showToast("First toast")
const topmost = showToast("Topmost toast")
const repeated = showToast("First toast")
expect(repeated).not.toBe(first)
toasterV2.dismiss(topmost)
toasterV2.dismiss(repeated)
toaster.dismiss(topmost)
toaster.dismiss(repeated)
})
test("creates no reactive computations at call time", () => {
@@ -50,7 +50,7 @@ describe("showToastV2", () => {
return undefined
}) as unknown as JSX.Element
const id = showToastV2({ description: "test", icon })
const id = showToast({ description: "test", icon })
// Resolving the icon at call time creates an ownerless computation that is
// never disposed and tracks its dependencies forever; it must only resolve
@@ -59,6 +59,6 @@ describe("showToastV2", () => {
setTick(1)
expect(reads).toBe(0)
toasterV2.dismiss(id)
toaster.dismiss(id)
})
})
+25 -4
View File
@@ -1,6 +1,5 @@
import { Argument, Command, Flag } from "effect/unstable/cli"
import { Argument, Flag } from "effect/unstable/cli"
import { Spec } from "../framework/spec"
import { GlobalFlags } from "./global-flags"
declare const OPENCODE_CLI_NAME: string | undefined
@@ -160,7 +159,29 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
}),
Spec.make("plugin", {
description: "Manage plugins",
commands: [Spec.make("list", { description: "List active plugins" })],
commands: [
Spec.make("list", {
description: "List plugins",
params: {
builtin: Flag.boolean("builtin").pipe(
Flag.withDescription("Include built-in server plugins"),
Flag.withDefault(false),
),
},
}),
Spec.make("add", {
description: "Install a plugin and add it to the global configuration",
params: {
package: Argument.string("package").pipe(Argument.withDescription("npm registry package specifier")),
},
}),
Spec.make("remove", {
description: "Remove a plugin from global configuration",
params: {
package: Argument.string("package").pipe(Argument.withDescription("configured package specifier")),
},
}),
],
}),
Spec.make("models", {
description: "List all available models",
@@ -321,4 +342,4 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
],
})
export const Commands = { ...Root, spec: Root.spec.pipe(Command.withGlobalFlags(GlobalFlags.all)) }
export const Commands = Root
-12
View File
@@ -1,12 +0,0 @@
export * as GlobalFlags from "./global-flags"
import { Flag, GlobalFlag } from "effect/unstable/cli"
export const CpuProfile = GlobalFlag.setting("cpu-profile")({
flag: Flag.string("cpu-profile").pipe(
Flag.withDescription("Write a CPU profile to this path when the process stops"),
Flag.optional,
),
})
export const all = [CpuProfile] as const
@@ -84,8 +84,12 @@ export default Runtime.handler(Commands, (input) =>
update: (update) => runPromise(config.update(update)),
},
packages: {
resolve: (spec) =>
runPromise(npm.add(spec, { subpaths: ["tui"] }).pipe(Effect.map((result) => result.entrypoint))),
resolve: (spec, install = true) =>
runPromise(
(install ? npm.add(spec, { subpaths: ["tui"] }) : npm.resolve(spec, { subpaths: ["tui"] })).pipe(
Effect.map((result) => result.entrypoint),
),
),
},
environment: requestedServer === undefined ? Env.session() : undefined,
terminalHandoff: () => preflight.finish(),
@@ -0,0 +1,82 @@
import { EOL } from "node:os"
import path from "node:path"
import { mkdir, readFile, rename, writeFile } from "node:fs/promises"
import { Effect } from "effect"
import { applyEdits, modify, parse, type ParseError } from "jsonc-parser"
import { Global } from "@opencode-ai/util/global"
import { Npm } from "@opencode-ai/util/npm"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { resolveConfigPath } from "../mcp/add"
import { Config } from "../../../config"
export default Runtime.handler(
Commands.commands.plugin.commands.add,
Effect.fn("cli.plugin.add")(function* (input) {
if (!(yield* Effect.promise(() => Npm.isRegistryPackage(input.package))))
return yield* Effect.fail(
new Error("Plugin target must be an npm registry package name, version, tag, or semver range"),
)
const npm = yield* Npm.Service
const installed = yield* npm.add(input.package, { subpaths: ["server", ""] })
const tui = yield* npm.resolve(input.package, { subpaths: ["tui"] })
const target = configurationTarget(installed.entrypoint, tui.entrypoint)
if (!target)
return yield* Effect.fail(new Error(`Plugin package has no server or TUI entrypoint: ${input.package}`))
if (target === "server") {
const global = yield* Global.Service
const configPath = yield* Effect.promise(() => resolveConfigPath(global.config))
const changed = yield* Effect.promise(() => writePluginConfig(configPath, input.package))
process.stdout.write(
changed
? `Plugin "${input.package}" installed and added to ${configPath}${EOL}`
: `Plugin "${input.package}" is already configured in ${configPath}${EOL}`,
)
return
}
const config = yield* Config.Service
yield* config.update((draft) => {
if (configured(draft.plugins, input.package)) return
draft.plugins = [...(draft.plugins ?? []), input.package]
})
process.stdout.write(`TUI plugin "${input.package}" installed and added to ${config.path}${EOL}`)
}),
)
export function configurationTarget(server?: string, tui?: string) {
if (server) return "server" as const
if (tui) return "tui" as const
}
export async function writePluginConfig(configPath: string, spec: string) {
const text = await readFile(configPath, "utf8").catch((error) => {
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return "{}"
throw error
})
const errors: ParseError[] = []
const config: unknown = parse(text, errors, { allowTrailingComma: true })
if (errors.length || typeof config !== "object" || config === null || Array.isArray(config))
throw new Error(`Invalid global configuration: ${configPath}`)
const plugins = "plugins" in config ? config.plugins : undefined
if (plugins !== undefined && !Array.isArray(plugins)) throw new Error(`Invalid plugins configuration: ${configPath}`)
if (configured(plugins, spec)) return false
const updated = applyEdits(
text,
modify(text, ["plugins"], [...(plugins ?? []), spec], { formattingOptions: { tabSize: 2, insertSpaces: true } }),
)
await mkdir(path.dirname(configPath), { recursive: true })
const temporary = configPath + ".tmp"
await writeFile(temporary, updated.endsWith("\n") ? updated : updated + "\n", { mode: 0o600 })
await rename(temporary, configPath)
return true
}
function configured(plugins: readonly unknown[] | undefined, spec: string) {
return plugins?.some(
(entry) =>
entry === spec || (typeof entry === "object" && entry !== null && "package" in entry && entry.package === spec),
)
}

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