mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-18 06:46:24 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4ade62900 | ||
|
|
0ac458b3b3 | ||
|
|
b278ef6b82 | ||
|
|
3e3a4ae46b | ||
|
|
fe0d9579a7 | ||
|
|
a2594ddefb | ||
|
|
3555cef1c8 | ||
|
|
0fa881efd0 | ||
|
|
37ff501cc6 | ||
|
|
fa126d68e8 |
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@opencode/ai",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -113,7 +113,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"bin": {
|
||||
"opencode2": "./bin/opencode2.cjs",
|
||||
},
|
||||
@@ -177,7 +177,7 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@opencode/protocol": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -203,7 +203,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -217,7 +217,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -253,7 +253,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode/console-core",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -280,7 +280,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -297,7 +297,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode/console-mail",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -321,7 +321,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -341,7 +341,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode/core",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
@@ -409,7 +409,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode/desktop",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"electron-context-menu": "4.1.2",
|
||||
@@ -461,7 +461,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/sdk": "1.18.21",
|
||||
@@ -498,7 +498,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode/function",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -514,7 +514,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode/http-recorder",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
},
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"packages/httpapi-codegen": {
|
||||
"name": "@opencode/httpapi-codegen",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"prettier": "3.6.2",
|
||||
@@ -546,7 +546,7 @@
|
||||
},
|
||||
"packages/latex": {
|
||||
"name": "@opencode/latex",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -560,7 +560,7 @@
|
||||
},
|
||||
"packages/merman": {
|
||||
"name": "@opencode/merman",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -575,7 +575,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode/plugin",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode/ai": "workspace:*",
|
||||
@@ -614,7 +614,7 @@
|
||||
},
|
||||
"packages/plugin-browser": {
|
||||
"name": "@opencode/plugin-browser",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -644,7 +644,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@opencode/protocol",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@opencode/schema": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -659,7 +659,7 @@
|
||||
},
|
||||
"packages/schema": {
|
||||
"name": "@opencode/schema",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@opencode/sdk",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -704,7 +704,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode/server",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/platform-node-shared": "catalog:",
|
||||
@@ -726,7 +726,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode/client": "workspace:*",
|
||||
@@ -761,7 +761,7 @@
|
||||
},
|
||||
"packages/simulation": {
|
||||
"name": "@opencode/simulation",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@opencode/ai": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -781,7 +781,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode/stats-app",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -815,7 +815,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode/stats-core",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -834,7 +834,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode/stats-server",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -880,7 +880,7 @@
|
||||
},
|
||||
"packages/theme": {
|
||||
"name": "@opencode/theme",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -894,7 +894,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode/tui",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -929,7 +929,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode/ui",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -964,7 +964,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode/util",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -997,7 +997,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode/web",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1038,7 +1038,7 @@
|
||||
},
|
||||
"services/update": {
|
||||
"name": "@opencode/update",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"jose": "6.0.11",
|
||||
"semver": "catalog:",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "opencode",
|
||||
"description": "AI-powered development tool",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.4.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"name": "@opencode/ai",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -13,7 +13,6 @@ import { HttpServer } from "effect/unstable/http"
|
||||
import { Env } from "./env"
|
||||
import { ServiceConfig } from "./services/service-config"
|
||||
import { ServiceRegistration } from "./services/service-registration"
|
||||
import { Updater } from "./services/updater"
|
||||
import { WebUi } from "./services/web-ui"
|
||||
import { databasePath } from "./database-path"
|
||||
|
||||
@@ -159,21 +158,6 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
const url = HttpServer.formatAddress(server.address)
|
||||
console.log(options.mode === "stdio" ? JSON.stringify({ url }) : `server listening on ${url}`)
|
||||
if (foreground && !environmentPassword) console.log(`server password ${password}`)
|
||||
yield* Updater.Service.pipe(
|
||||
Effect.flatMap((updater) =>
|
||||
Updater.pollUpdates({
|
||||
check: updater.run().pipe(
|
||||
Effect.flatMap((result) => {
|
||||
if (!result) return Effect.void
|
||||
if (result.type === "available") return server.updateAvailable(result.version)
|
||||
return server.updated(result.version)
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
Effect.provide(Updater.layer),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
return yield* options.mode === "service"
|
||||
? server.shutdown
|
||||
: options.mode === "stdio"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Global } from "@opencode/util/global"
|
||||
import { AppProcess } from "@opencode/util/process"
|
||||
import { OPENCODE_ARTIFACT, OPENCODE_CHANNEL, OPENCODE_LOCAL, OPENCODE_VERSION } from "../version"
|
||||
import { Context, Duration, Effect, FileSystem, Layer, Ref, Schedule } from "effect"
|
||||
import { Context, Duration, Effect, FileSystem, Layer, Ref } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { parse, type ParseError } from "jsonc-parser"
|
||||
import path from "node:path"
|
||||
@@ -24,18 +24,6 @@ export interface Interface {
|
||||
| undefined
|
||||
}
|
||||
|
||||
export const pollUpdates = Effect.fnUntraced(function* (input: {
|
||||
readonly check: Effect.Effect<unknown>
|
||||
readonly initialDelay?: Duration.Input
|
||||
readonly interval?: Duration.Input
|
||||
}) {
|
||||
const interval = input.interval ?? "10 minutes"
|
||||
return yield* input.check.pipe(
|
||||
Effect.repeat(Schedule.spaced(interval)),
|
||||
Effect.delay(input.initialDelay ?? "1 minute"),
|
||||
)
|
||||
})
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/cli/Updater") {}
|
||||
|
||||
export function decodePolicy(text: string): Policy | undefined {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect, Layer, Queue } from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
import { testEffect } from "../../core/test/lib/effect"
|
||||
import { Updater } from "../src/services/updater"
|
||||
|
||||
const it = testEffect(Layer.empty)
|
||||
|
||||
it.effect("polls after 1 minute and every 10 minutes after that", () =>
|
||||
Effect.gen(function* () {
|
||||
const checks = yield* Queue.unbounded<void>()
|
||||
yield* Updater.pollUpdates({ check: Queue.offer(checks, undefined).pipe(Effect.asVoid) }).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Effect.yieldNow
|
||||
expect(yield* Queue.size(checks)).toBe(0)
|
||||
yield* TestClock.adjust("59 seconds")
|
||||
expect(yield* Queue.size(checks)).toBe(0)
|
||||
yield* TestClock.adjust("1 second")
|
||||
yield* Queue.take(checks)
|
||||
yield* Effect.yieldNow
|
||||
yield* TestClock.adjust("10 minutes")
|
||||
yield* Queue.take(checks)
|
||||
}),
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -2135,7 +2135,7 @@ export type ConfigEntry =
|
||||
experimental?: {
|
||||
portable_shell_scanner?: boolean
|
||||
subagent_depth?: number
|
||||
policies?: Array<{ action: "provider.use"; resource: string; effect: "allow" | "deny" }>
|
||||
policies?: Array<{ action: "provider.use" | "permission"; resource: string; effect: "allow" | "deny" }>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/console-core",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-mail",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"name": "@opencode/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -12,16 +12,34 @@ export const Plugin = define({
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, ctx.provider.reload())
|
||||
yield* ctx.provider.transform((providers) => {
|
||||
// User-global policy takes priority over policy authored by a repository.
|
||||
const policies = loaded.entries
|
||||
const policies = () =>
|
||||
loaded.entries
|
||||
.filter((entry): entry is Document => entry.type === "document")
|
||||
.toReversed()
|
||||
.flatMap((entry) => entry.info.experimental?.policies ?? [])
|
||||
yield* ctx.provider.transform((providers) => {
|
||||
// User-global policy takes priority over policy authored by a repository.
|
||||
for (const record of providers.list()) {
|
||||
const policy = policies.findLast((policy) => Wildcard.match(record.provider.id, policy.resource))
|
||||
const policy = policies().findLast(
|
||||
(policy) => policy.action === "provider.use" && Wildcard.match(record.provider.id, policy.resource),
|
||||
)
|
||||
if (policy?.effect === "deny") providers.remove(record.provider.id)
|
||||
}
|
||||
})
|
||||
yield* ctx.permission.hook("evaluate", (event) =>
|
||||
Effect.sync(() => {
|
||||
const current = policies()
|
||||
const denied = event.resources.some((resource) => {
|
||||
const policy = current.findLast(
|
||||
(policy) =>
|
||||
policy.action === "permission" && Wildcard.match(`${event.action}:${resource}`, policy.resource),
|
||||
)
|
||||
return policy?.effect === "deny"
|
||||
})
|
||||
if (!denied) return
|
||||
event.effect = "deny"
|
||||
event.message = "Blocked by configuration policy"
|
||||
}),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -76,7 +76,7 @@ import { SessionSystemPrompt } from "@opencode/core/session/system-prompt"
|
||||
import { ID, Model } from "@opencode/core/model"
|
||||
import { Location } from "@opencode/core/location"
|
||||
import { Provider } from "@opencode/core/provider"
|
||||
import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, Queue, Schema, Scope, Stream } from "effect"
|
||||
import { Cause, Context, DateTime, Deferred, Effect, Exit, Fiber, Layer, Queue, Schema, Scope, Stream } from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
import { asc, desc, eq, sql } from "drizzle-orm"
|
||||
import { testEffect } from "./lib/effect"
|
||||
@@ -3391,11 +3391,17 @@ describe("SessionRunnerLLM", () => {
|
||||
|
||||
scenario("consumes the full provider stream before recording its boundary and settling local tools", function* (s) {
|
||||
yield* s.admit("Echo this")
|
||||
const request = yield* s.llm.gate
|
||||
const tail = yield* Deferred.make<void>()
|
||||
const complete = yield* Deferred.make<void>()
|
||||
const finished = yield* Deferred.make<void>()
|
||||
yield* s.llm.push(
|
||||
Stream.fromIterable(TestLLM.tool("call-streamed", "echo", { text: "hello" })).pipe(
|
||||
Stream.fromIterable(
|
||||
TestLLM.complete(
|
||||
{ reason: { normalized: "tool-calls" }, usage: { outputTokens: 100, reasoningTokens: 80 } },
|
||||
LLMEvent.toolCall({ id: "call-streamed", name: "echo", input: { text: "hello" } }),
|
||||
),
|
||||
).pipe(
|
||||
Stream.concat(
|
||||
Stream.fromEffect(Deferred.succeed(tail, undefined).pipe(Effect.andThen(Deferred.await(complete)))).pipe(
|
||||
Stream.drain,
|
||||
@@ -3413,25 +3419,39 @@ describe("SessionRunnerLLM", () => {
|
||||
)
|
||||
const run = yield* Effect.forkChild(s.resume)
|
||||
|
||||
yield* request.started
|
||||
yield* TestClock.adjust("2 seconds")
|
||||
yield* request.release
|
||||
yield* tools.started
|
||||
yield* Deferred.await(tail)
|
||||
expect(s.requests).toHaveLength(1)
|
||||
expect(yield* recordedEventTypes(sessionID)).not.toContain("session.step.streamed.1")
|
||||
expect(requireAssistant(yield* s.context).time.completed).toBeUndefined()
|
||||
yield* TestClock.adjust("3 seconds")
|
||||
yield* Deferred.succeed(complete, undefined)
|
||||
yield* Fiber.join(streamed)
|
||||
expect(yield* Deferred.isDone(finished)).toBe(true)
|
||||
const assistant = requireAssistant(yield* s.context)
|
||||
expect(assistant.time.streamed).toBeDefined()
|
||||
expect(DateTime.toEpochMillis(assistant.time.streamed!) - DateTime.toEpochMillis(assistant.time.created)).toBe(
|
||||
5_000,
|
||||
)
|
||||
expect(assistant.time.completed).toBeUndefined()
|
||||
expect(assistant.content).toMatchObject([{ type: "tool", state: { status: "running" } }])
|
||||
|
||||
yield* TestClock.adjust("10 seconds")
|
||||
yield* tools.release
|
||||
yield* Fiber.join(run)
|
||||
const events = yield* recordedEventTypes(sessionID)
|
||||
expect(events.indexOf("session.step.streamed.1")).toBeLessThan(events.indexOf("session.tool.success.2"))
|
||||
expect(events.indexOf("session.tool.success.2")).toBeLessThan(events.indexOf("session.step.ended.1"))
|
||||
expect(events.filter((type) => type === "session.step.streamed.1")).toHaveLength(2)
|
||||
yield* replaySessionProjection(sessionID)
|
||||
const replayed = (yield* s.context).find((message) => message.id === assistant.id)
|
||||
expect(replayed).toMatchObject({
|
||||
time: { created: assistant.time.created, streamed: assistant.time.streamed },
|
||||
tokens: { output: 20, reasoning: 80 },
|
||||
})
|
||||
})
|
||||
|
||||
scenario("restores durable reasoning provider metadata in the next request", function* (s) {
|
||||
@@ -5037,14 +5057,18 @@ describe("SessionRunnerLLM", () => {
|
||||
scenario(`bounds jittered exponential backoff before output for ${failure.name}`, function* (s) {
|
||||
yield* s.admit("Retry transport")
|
||||
yield* s.llm.push(TestLLM.failAfter(failure(), LLMEvent.stepStart({ index: 0 })))
|
||||
yield* s.llm.push(TestLLM.text("Recovered", "retry-success"))
|
||||
yield* s.llm.push(
|
||||
Stream.fromEffect(Effect.sleep(400)).pipe(
|
||||
Stream.flatMap(() => Stream.fromIterable(TestLLM.text("Recovered", "retry-success"))),
|
||||
),
|
||||
)
|
||||
|
||||
const scheduled = yield* subscribeRetries(s)
|
||||
const run = yield* s.resume.pipe(Effect.forkChild)
|
||||
yield* Queue.take(scheduled)
|
||||
yield* TestClock.adjust("1599 millis")
|
||||
expect(s.requests).toHaveLength(1)
|
||||
yield* TestClock.adjust("801 millis")
|
||||
yield* TestClock.adjust("1201 millis")
|
||||
yield* Fiber.join(run)
|
||||
|
||||
expect(s.requests).toHaveLength(2)
|
||||
@@ -5058,6 +5082,10 @@ describe("SessionRunnerLLM", () => {
|
||||
])
|
||||
yield* replaySessionProjection(sessionID)
|
||||
expect((yield* s.context).filter((message) => message.type === "assistant")).toHaveLength(1)
|
||||
const assistant = requireAssistant(yield* s.context)
|
||||
expect(DateTime.toEpochMillis(assistant.time.streamed!) - DateTime.toEpochMillis(assistant.time.created)).toBe(
|
||||
400,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode/desktop",
|
||||
"private": true,
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/function",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"name": "@opencode/http-recorder",
|
||||
"description": "Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/httpapi-codegen",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/latex",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/merman",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/plugin-browser",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"description": "OpenCode's desktop browser plugin",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/plugin",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/protocol",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/schema",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -6,7 +6,7 @@ export const Effect = Schema.Literals(["allow", "deny"])
|
||||
export type Effect = typeof Effect.Type
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
action: Schema.Literal("provider.use"),
|
||||
action: Schema.Literals(["provider.use", "permission"]),
|
||||
resource: Schema.String,
|
||||
effect: Effect,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"name": "@opencode/sdk",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/server",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/simulation",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/stats-app",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/stats-core",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/stats-server",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/theme",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -136,9 +136,10 @@ export const DEFAULT_THEME = {
|
||||
},
|
||||
background: {
|
||||
default: "$hue.neutral.200",
|
||||
surface: {
|
||||
offset: "$hue.neutral.300",
|
||||
overlay: "$hue.neutral.400",
|
||||
raised: {
|
||||
base: "$hue.neutral.300",
|
||||
high: "$hue.neutral.400",
|
||||
max: "$hue.neutral.500",
|
||||
},
|
||||
action: {
|
||||
primary: {
|
||||
@@ -161,7 +162,7 @@ export const DEFAULT_THEME = {
|
||||
},
|
||||
formfield: {
|
||||
default: "$background.default",
|
||||
$hovered: "$background.surface.offset",
|
||||
$hovered: "$background.raised.base",
|
||||
$focused: "$background.action.primary.default",
|
||||
$pressed: "$hue.interactive.800",
|
||||
$disabled: "$background.default",
|
||||
@@ -217,20 +218,6 @@ export const DEFAULT_THEME = {
|
||||
imageText: "$hue.cyan.600",
|
||||
codeBlock: "$hue.neutral.900",
|
||||
},
|
||||
"@context:elevated": {
|
||||
text: { action: { primary: { default: "$hue.neutral.100" } } },
|
||||
background: {
|
||||
default: "$background.surface.offset",
|
||||
action: { primary: { default: "$hue.interactive.500", $hovered: "$background.surface.overlay" } },
|
||||
},
|
||||
},
|
||||
"@context:overlay": {
|
||||
text: { action: { primary: { default: "$hue.neutral.100" } } },
|
||||
background: {
|
||||
default: "$background.surface.overlay",
|
||||
action: { primary: { default: "$hue.interactive.500" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
hue: {
|
||||
@@ -357,9 +344,10 @@ export const DEFAULT_THEME = {
|
||||
},
|
||||
background: {
|
||||
default: "$hue.neutral.800",
|
||||
surface: {
|
||||
offset: "$hue.neutral.700",
|
||||
overlay: "$hue.neutral.600",
|
||||
raised: {
|
||||
base: "$hue.neutral.700",
|
||||
high: "$hue.neutral.600",
|
||||
max: "$hue.neutral.500",
|
||||
},
|
||||
action: {
|
||||
primary: {
|
||||
@@ -382,7 +370,7 @@ export const DEFAULT_THEME = {
|
||||
},
|
||||
formfield: {
|
||||
default: "$background.default",
|
||||
$hovered: "$background.surface.offset",
|
||||
$hovered: "$background.raised.base",
|
||||
$focused: "$background.action.primary.default",
|
||||
$pressed: "$hue.interactive.800",
|
||||
$disabled: "$background.default",
|
||||
@@ -438,19 +426,5 @@ export const DEFAULT_THEME = {
|
||||
imageText: "$hue.cyan.400",
|
||||
codeBlock: "$hue.neutral.100",
|
||||
},
|
||||
"@context:elevated": {
|
||||
text: { action: { primary: { default: "$hue.neutral.200" } } },
|
||||
background: {
|
||||
default: "$background.surface.offset",
|
||||
action: { primary: { default: "$hue.interactive.400", $hovered: "$background.surface.overlay" } },
|
||||
},
|
||||
},
|
||||
"@context:overlay": {
|
||||
text: { action: { primary: { default: "$hue.neutral.200" } } },
|
||||
background: {
|
||||
default: "$background.surface.overlay",
|
||||
action: { primary: { default: "$hue.interactive.400" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies ThemeDocument
|
||||
|
||||
@@ -8,15 +8,7 @@ import type {
|
||||
import { ActionState } from "./schema.js"
|
||||
|
||||
export function expandTheme<Definition extends ModeDefinition>(definition: Definition): Definition {
|
||||
return {
|
||||
...definition,
|
||||
...expandTokens(definition),
|
||||
...Object.fromEntries(
|
||||
Object.entries(definition)
|
||||
.filter(([key]) => key.startsWith("@context:"))
|
||||
.map(([key, value]) => [key, expandTokens(value as ThemeTokensDefinition)]),
|
||||
),
|
||||
}
|
||||
return { ...definition, ...expandTokens(definition) }
|
||||
}
|
||||
|
||||
export function expandTokens(definition: ThemeTokensDefinition): ThemeTokensDefinition {
|
||||
|
||||
@@ -15,7 +15,7 @@ export function fallback(mode: Mode): ThemeTokensDefinition {
|
||||
},
|
||||
background: {
|
||||
default: red,
|
||||
surface: { offset: red, overlay: red },
|
||||
raised: { base: red, high: red, max: red },
|
||||
action: Object.fromEntries(ActionVariant.literals.map((variant) => [variant, { default: red }])),
|
||||
formfield: { default: red },
|
||||
feedback: Object.fromEntries(FeedbackKind.literals.map((kind) => [kind, { default: red }])),
|
||||
|
||||
@@ -26,14 +26,12 @@ export {
|
||||
type MergeModeDefinition,
|
||||
type Mode,
|
||||
type StatefulColorDefinition,
|
||||
type ContextKey,
|
||||
type TextDefinition,
|
||||
type ThemeTokensDefinition,
|
||||
} from "./schema.js"
|
||||
|
||||
export type {
|
||||
Categorical,
|
||||
ContextName,
|
||||
FormfieldColor,
|
||||
Hue,
|
||||
HueSource,
|
||||
@@ -43,6 +41,7 @@ export type {
|
||||
ResolvedTheme,
|
||||
ResolvedThemeTokens,
|
||||
StatefulColor,
|
||||
SurfaceName,
|
||||
} from "./types.js"
|
||||
export { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "./defaults.js"
|
||||
export { expandTheme } from "./expand.js"
|
||||
|
||||
@@ -3,25 +3,14 @@ import { Schema } from "effect"
|
||||
import { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "./defaults.js"
|
||||
import { expandTheme, expandTokens, mergeTheme } from "./expand.js"
|
||||
import { fallback } from "./fallback.js"
|
||||
import {
|
||||
ActionState,
|
||||
ActionVariant,
|
||||
BaseHue,
|
||||
FeedbackKind,
|
||||
HueAlias,
|
||||
HueStep,
|
||||
ThemeDefinition,
|
||||
ThemeDocument,
|
||||
} from "./schema.js"
|
||||
import { ActionState, BaseHue, HueAlias, HueStep, ThemeDefinition, ThemeDocument } from "./schema.js"
|
||||
import type {
|
||||
ActionStateKey,
|
||||
ContextName,
|
||||
HueDefinition,
|
||||
HueScale,
|
||||
ResolvedActionState,
|
||||
Mode,
|
||||
ResolvedTheme,
|
||||
ResolvedThemeTokens,
|
||||
StatefulColorDefinition,
|
||||
SurfaceName,
|
||||
ThemeTokensDefinition,
|
||||
} from "./index.js"
|
||||
import { selectTheme, selectThemeMode } from "./select.js"
|
||||
@@ -42,40 +31,54 @@ export function themeDecodeError(error: unknown, name: string) {
|
||||
return new Error(`Invalid theme: ${name} ${value} is an invalid value`, { cause: error })
|
||||
}
|
||||
|
||||
export function resolveThemeDocument(document: ThemeDocument, mode?: "light" | "dark") {
|
||||
export function resolveThemeDocument(document: ThemeDocument, mode?: Mode) {
|
||||
const selected = selectThemeMode(document, mode)
|
||||
const definition = selected.expanded ? selected.theme : expandTheme(selected.theme)
|
||||
const defaults = expandTheme(selectTheme(DEFAULT_THEME, selected.mode))
|
||||
const core = expandTokens(fallback(selected.mode))
|
||||
const merged = document.standalone ? mergeTheme(core, definition) : mergeTheme(core, defaults, definition)
|
||||
if (!merged["hue"]) throw new Error("Standalone themes must provide hues")
|
||||
return resolveExpandedTheme({
|
||||
...merged,
|
||||
categorical: merged["categorical"] ?? DEFAULT_CATEGORICAL,
|
||||
} as ThemeDefinition)
|
||||
return resolveExpandedTheme(
|
||||
{
|
||||
...merged,
|
||||
categorical: merged["categorical"] ?? DEFAULT_CATEGORICAL,
|
||||
} as ThemeDefinition,
|
||||
selected.mode,
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveTheme(definition: ThemeDefinition): ResolvedTheme {
|
||||
return resolveExpandedTheme(expandTheme(decodeThemeDefinition(definition)))
|
||||
export function resolveTheme(definition: ThemeDefinition, mode: Mode): ResolvedTheme {
|
||||
return resolveExpandedTheme(expandTheme(decodeThemeDefinition(definition)), mode)
|
||||
}
|
||||
|
||||
function resolveExpandedTheme(definition: ThemeDefinition): ResolvedTheme {
|
||||
// Surfaces are code-owned: a theme file describes one palette, and each surface is that palette
|
||||
// re-resolved with a raised background so `$background.default` references follow it.
|
||||
const SURFACES: Record<SurfaceName, ThemeTokensDefinition> = {
|
||||
raised: {
|
||||
background: {
|
||||
default: "$background.raised.base",
|
||||
action: { primary: { $hovered: "$background.raised.high" } },
|
||||
},
|
||||
},
|
||||
overlay: { background: { default: "$background.raised.high" } },
|
||||
}
|
||||
|
||||
function resolveExpandedTheme(definition: ThemeDefinition, mode: Mode): ResolvedTheme {
|
||||
const hue = resolveHue(definition.hue)
|
||||
const categorical = (definition.categorical ?? DEFAULT_CATEGORICAL).map((name) => hue[name])
|
||||
const hueSteps = compileHueSteps(hue)
|
||||
const raise = (color: RGBA) => (mode === "light" ? hueSteps.increase(color) : hueSteps.decrease(color))
|
||||
const base = tokens(definition)
|
||||
const resolved = resolveView(base, hue, categorical, hueSteps)
|
||||
const context = (name: ContextName) => {
|
||||
const override = definition[`@context:${name}`]
|
||||
if (!override) return resolved
|
||||
return resolveView(contextualize(base, override), hue, categorical, hueSteps)
|
||||
const views = {} as Record<SurfaceName, ResolvedTheme>
|
||||
const view = (tokens: ThemeTokensDefinition): ResolvedTheme => ({
|
||||
...resolveView(tokens, hue, categorical, hueSteps),
|
||||
raise,
|
||||
surface: (name) => views[name],
|
||||
})
|
||||
for (const name of Object.keys(SURFACES) as SurfaceName[]) {
|
||||
views[name] = view(mergeTheme(base, SURFACES[name]) as ThemeTokensDefinition)
|
||||
}
|
||||
const contextual = {
|
||||
elevated: context("elevated"),
|
||||
overlay: context("overlay"),
|
||||
}
|
||||
|
||||
return { ...resolved, contextual } as ResolvedTheme
|
||||
return view(base)
|
||||
}
|
||||
|
||||
function tokens(definition: ThemeDefinition): ThemeTokensDefinition {
|
||||
@@ -90,48 +93,6 @@ function tokens(definition: ThemeDefinition): ThemeTokensDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
function contextualize(base: ThemeTokensDefinition, override: ThemeTokensDefinition) {
|
||||
const result = mergeTheme(base, override)
|
||||
const baseText = base.text?.action
|
||||
const contextText = override.text?.action
|
||||
const baseBackground = base.background?.action
|
||||
const contextBackground = override.background?.action
|
||||
const text = result["text"] as NonNullable<ThemeTokensDefinition["text"]>
|
||||
const background = result["background"] as NonNullable<ThemeTokensDefinition["background"]>
|
||||
return {
|
||||
...result,
|
||||
text: { ...text, action: contextualActions(baseText, contextText) },
|
||||
background: { ...background, action: contextualActions(baseBackground, contextBackground) },
|
||||
} as ThemeTokensDefinition
|
||||
}
|
||||
|
||||
function contextualActions(
|
||||
base: Partial<Record<ActionVariant, StatefulColorDefinition>> | undefined,
|
||||
context: Partial<Record<ActionVariant, StatefulColorDefinition>> | undefined,
|
||||
) {
|
||||
return Object.fromEntries(
|
||||
ActionVariant.literals.map((variant) => {
|
||||
const baseVariant = base?.[variant]
|
||||
const contextVariant = context?.[variant]
|
||||
return [
|
||||
variant,
|
||||
Object.fromEntries(
|
||||
(["default", ...ActionState.literals] as readonly ResolvedActionState[]).map((state) => {
|
||||
const key = state === "default" ? undefined : (`$${state}` as ActionStateKey)
|
||||
return [
|
||||
key ?? "default",
|
||||
(key ? contextVariant?.[key] : undefined) ??
|
||||
contextVariant?.default ??
|
||||
(key ? baseVariant?.[key] : undefined) ??
|
||||
baseVariant?.default,
|
||||
]
|
||||
}),
|
||||
),
|
||||
]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function resolveView(
|
||||
definition: ThemeTokensDefinition,
|
||||
hue: ResolvedThemeTokens["hue"],
|
||||
|
||||
@@ -40,9 +40,6 @@ export const CategoricalDefinition = Schema.Array(HueName).check(Schema.isMinLen
|
||||
export type CategoricalDefinition = Schema.Schema.Type<typeof CategoricalDefinition>
|
||||
const HueColorValue = Schema.Union([HexColor, Schema.TemplateLiteral(["$hue.", HueName, ".", HueStep])])
|
||||
|
||||
const ContextKey = Schema.Literals(["@context:elevated", "@context:overlay"])
|
||||
export type ContextKey = Schema.Schema.Type<typeof ContextKey>
|
||||
|
||||
const HueScaleDefinition = Schema.Record(HueStep, HexColor)
|
||||
const HueValueDefinition = Schema.Union([Schema.TemplateLiteral(["$hue.", HueName]), HueScaleDefinition])
|
||||
|
||||
@@ -129,10 +126,11 @@ export type TextDefinition = Schema.Schema.Type<typeof TextDefinition>
|
||||
|
||||
const BackgroundDefinition = Schema.Struct({
|
||||
default: Schema.optional(ColorValue),
|
||||
surface: Schema.optional(
|
||||
raised: Schema.optional(
|
||||
Schema.Struct({
|
||||
offset: Schema.optional(ColorValue),
|
||||
overlay: Schema.optional(ColorValue),
|
||||
base: Schema.optional(ColorValue),
|
||||
high: Schema.optional(ColorValue),
|
||||
max: Schema.optional(ColorValue),
|
||||
}),
|
||||
),
|
||||
action: Schema.optional(ActionColorDefinition),
|
||||
@@ -228,8 +226,6 @@ const ThemeDefinitionFields = Schema.Struct({
|
||||
hue: HueDefinition,
|
||||
categorical: Schema.optional(CategoricalDefinition),
|
||||
...ThemeTokensDefinition.fields,
|
||||
"@context:elevated": Schema.optional(ThemeTokensDefinition),
|
||||
"@context:overlay": Schema.optional(ThemeTokensDefinition),
|
||||
})
|
||||
export const ThemeDefinition = ThemeDefinitionFields
|
||||
export type ThemeDefinition = Schema.Schema.Type<typeof ThemeDefinition>
|
||||
@@ -238,8 +234,6 @@ const FileThemeDefinition = Schema.Struct({
|
||||
hue: Schema.optional(HueOverrideDefinition),
|
||||
categorical: Schema.optional(CategoricalDefinition),
|
||||
...ThemeTokensDefinition.fields,
|
||||
"@context:elevated": Schema.optional(ThemeTokensDefinition),
|
||||
"@context:overlay": Schema.optional(ThemeTokensDefinition),
|
||||
})
|
||||
export type FileThemeDefinition = Schema.Schema.Type<typeof FileThemeDefinition>
|
||||
|
||||
@@ -248,8 +242,6 @@ const MergeModeDefinition = Schema.Struct({
|
||||
hue: Schema.optional(HueOverrideDefinition),
|
||||
categorical: Schema.optional(CategoricalDefinition),
|
||||
...ThemeTokensDefinition.fields,
|
||||
"@context:elevated": Schema.optional(ThemeTokensDefinition),
|
||||
"@context:overlay": Schema.optional(ThemeTokensDefinition),
|
||||
})
|
||||
export type MergeModeDefinition = Schema.Schema.Type<typeof MergeModeDefinition>
|
||||
export const ModeDefinition = Schema.Union([MergeModeDefinition, FileThemeDefinition])
|
||||
|
||||
@@ -40,9 +40,10 @@ export type ResolvedThemeTokens = {
|
||||
}
|
||||
readonly background: {
|
||||
readonly default: RGBA
|
||||
readonly surface: {
|
||||
readonly offset: RGBA
|
||||
readonly overlay: RGBA
|
||||
readonly raised: {
|
||||
readonly base: RGBA
|
||||
readonly high: RGBA
|
||||
readonly max: RGBA
|
||||
}
|
||||
readonly action: Readonly<Record<ActionVariant, StatefulColor>>
|
||||
readonly formfield: FormfieldColor
|
||||
@@ -68,8 +69,11 @@ export type ResolvedThemeTokens = {
|
||||
readonly markdown: Readonly<Record<MarkdownToken, RGBA>>
|
||||
}
|
||||
|
||||
export type ContextName = "elevated" | "overlay"
|
||||
export type SurfaceName = "raised" | "overlay"
|
||||
|
||||
export type ResolvedTheme = ResolvedThemeTokens & {
|
||||
readonly contextual: Readonly<Record<ContextName, ResolvedThemeTokens>>
|
||||
/** Lift a ramp color one step away from the background: lighter in light mode, darker in dark mode. */
|
||||
readonly raise: (color: RGBA) => RGBA
|
||||
/** The same theme re-resolved on a raised surface. Absolute: every view's surfaces are the base theme's. */
|
||||
readonly surface: (name: SurfaceName) => ResolvedTheme
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ function migrateMode(theme: Theme, mode: Mode): FileThemeDefinition {
|
||||
const background = mode === "light" ? "$hue.neutral.200" : "$hue.neutral.800"
|
||||
const backgroundPanel = mode === "light" ? "$hue.neutral.300" : "$hue.neutral.700"
|
||||
const backgroundMenu = mode === "light" ? "$hue.neutral.400" : "$hue.neutral.600"
|
||||
const backgroundRaisedMax = "$hue.neutral.500"
|
||||
|
||||
return referenceHues({
|
||||
hue: {
|
||||
@@ -102,9 +103,10 @@ function migrateMode(theme: Theme, mode: Mode): FileThemeDefinition {
|
||||
},
|
||||
background: {
|
||||
default: background,
|
||||
surface: {
|
||||
offset: backgroundPanel,
|
||||
overlay: backgroundMenu,
|
||||
raised: {
|
||||
base: backgroundPanel,
|
||||
high: backgroundMenu,
|
||||
max: backgroundRaisedMax,
|
||||
},
|
||||
action: {
|
||||
primary: { default: "transparent", $hovered: backgroundPanel, $focused: primary, $selected: "transparent" },
|
||||
@@ -171,13 +173,6 @@ function migrateMode(theme: Theme, mode: Mode): FileThemeDefinition {
|
||||
imageText: color("markdownImageText"),
|
||||
codeBlock: color("markdownCodeBlock"),
|
||||
},
|
||||
"@context:elevated": {
|
||||
background: {
|
||||
default: "$background.surface.offset",
|
||||
action: { primary: { $hovered: "$background.surface.overlay" } },
|
||||
},
|
||||
},
|
||||
"@context:overlay": { background: { default: "$background.surface.overlay" } },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ test.each(["light", "dark"] as const)("built-in %s themes resolve status colors"
|
||||
expect(theme.text.status.question.equals(theme.text.status.unread)).toBeTrue()
|
||||
expect(theme.text.status.permission.equals(theme.text.status.unread)).toBeTrue()
|
||||
expect(theme.text.status.unread.equals(theme.hue.accent[mode === "light" ? 800 : 200])).toBeTrue()
|
||||
expect(theme.contextual.elevated.text.status).toEqual(theme.text.status)
|
||||
expect(theme.surface("raised").text.status).toEqual(theme.text.status)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ test.each(["light", "dark"] as const)("custom %s themes inherit the unread atten
|
||||
expect(theme.text.status.unread.equals(RGBA.fromHex("#abcdef"))).toBeTrue()
|
||||
expect(theme.text.status.question.equals(theme.text.status.unread)).toBeTrue()
|
||||
expect(theme.text.status.permission.equals(theme.text.status.unread)).toBeTrue()
|
||||
expect(theme.contextual.elevated.text.status).toEqual(theme.text.status)
|
||||
expect(theme.surface("raised").text.status).toEqual(theme.text.status)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/tui",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -39,7 +39,7 @@ export function DevToolsBar() {
|
||||
const renderer = useRenderer()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const { current: theme, mode, supports, setMode } = themes
|
||||
const elevatedTheme = useTheme("elevated")
|
||||
const elevatedTheme = useTheme().surface("raised")
|
||||
const [panel, setPanel] = createSignal<Panel>()
|
||||
const [dumping, setDumping] = createSignal(false)
|
||||
const [dumpPath, setDumpPath] = createSignal<string>()
|
||||
@@ -474,7 +474,7 @@ function BarItem(props: ParentProps<{ active: boolean; onClick: () => void }>) {
|
||||
}
|
||||
|
||||
function PanelBox(props: ParentProps) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const renderer = useRenderer()
|
||||
return (
|
||||
<box
|
||||
@@ -500,7 +500,7 @@ function PanelBox(props: ParentProps) {
|
||||
}
|
||||
|
||||
function PanelTitle(props: ParentProps) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
return (
|
||||
<text fg={theme.text.default} attributes={TextAttributes.BOLD} marginBottom={1}>
|
||||
{props.children}
|
||||
@@ -509,7 +509,7 @@ function PanelTitle(props: ParentProps) {
|
||||
}
|
||||
|
||||
function Row(props: { label: string; value: string }) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
return (
|
||||
<box flexDirection="row">
|
||||
<text fg={theme.text.subdued}>{props.label}</text>
|
||||
@@ -520,7 +520,7 @@ function Row(props: { label: string; value: string }) {
|
||||
}
|
||||
|
||||
function Action(props: ParentProps<{ onClick: () => void; disabled?: boolean; hoverBackground?: boolean }>) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const [hovered, setHovered] = createSignal(false)
|
||||
return (
|
||||
<box
|
||||
@@ -545,7 +545,7 @@ function cpuPercent(microseconds: number, milliseconds: number) {
|
||||
}
|
||||
|
||||
function ProcessStat(props: { label: string; values: readonly number[]; unit: string; decimals?: number }) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const value = () => {
|
||||
const value = props.values.at(-1)
|
||||
if (value === undefined) return "--"
|
||||
|
||||
@@ -27,7 +27,7 @@ export function DialogErrorDetails(props: {
|
||||
const location = useLocation()
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const renderer = useRenderer()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const config = useConfig().data
|
||||
|
||||
@@ -13,7 +13,7 @@ type ImagePreviewItem = Readonly<{
|
||||
export function DialogImagePreview(props: { images: readonly ImagePreviewItem[]; initial: number }) {
|
||||
const dialog = useDialog()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const [index, setIndex] = createSignal(Math.max(0, Math.min(props.images.length - 1, props.initial)))
|
||||
const [failed, setFailed] = createSignal(false)
|
||||
const current = createMemo(() => props.images[index()])
|
||||
|
||||
@@ -78,7 +78,7 @@ export function DialogIntegration(
|
||||
const data = useData()
|
||||
const currentLocation = useLocation()
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const location = currentLocation.ref ?? data.location.default()
|
||||
const integrations = createMemo(() =>
|
||||
integrationOptions(data.location.integration.list(location) ?? []).filter(
|
||||
@@ -153,7 +153,7 @@ function manageConnections(
|
||||
const data = useData()
|
||||
const client = useClient()
|
||||
const toast = useToast()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const shortcuts = Keymap.useShortcuts()
|
||||
const [deleting, setDeleting] = createSignal<string>()
|
||||
const [selected, setSelected] = createSignal(methods.length ? "add" : credentialConnections(integration)[0]?.id)
|
||||
@@ -427,8 +427,8 @@ function CommandPending(props: {
|
||||
|
||||
function CommandView(props: { title: string; output: string; message: string }) {
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
const overlayTheme = useTheme("overlay")
|
||||
const theme = useTheme().surface("raised")
|
||||
const overlayTheme = useTheme().surface("overlay")
|
||||
onMount(() => dialog.setSize("large"))
|
||||
return (
|
||||
<box gap={1} paddingBottom={1}>
|
||||
@@ -467,7 +467,7 @@ function KeyMethod(props: {
|
||||
const dialog = useDialog()
|
||||
const client = useClient()
|
||||
const toast = useToast()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const [error, setError] = createSignal<string>()
|
||||
|
||||
return (
|
||||
@@ -672,7 +672,7 @@ function OAuthCode(props: {
|
||||
const dialog = useDialog()
|
||||
const client = useClient()
|
||||
const toast = useToast()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const [error, setError] = createSignal<string>()
|
||||
let settled = false
|
||||
|
||||
@@ -724,7 +724,7 @@ function OAuthView(props: {
|
||||
open?: boolean
|
||||
}) {
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
@@ -852,7 +852,7 @@ function textAnswer(
|
||||
return new Promise<FormValue | undefined | typeof CANCELLED>((resolve) => {
|
||||
dialog.replace(
|
||||
() => {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const [error, setError] = createSignal<string>()
|
||||
return (
|
||||
<DialogPrompt
|
||||
|
||||
@@ -40,7 +40,7 @@ export function DialogMcp(props: { initialServer?: string; details?: boolean } =
|
||||
const client = useClient()
|
||||
const location = useLocation()
|
||||
const toast = useToast()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const current = () => location.ref ?? data.location.default()
|
||||
const servers = createMemo(() =>
|
||||
pipe(
|
||||
|
||||
@@ -44,7 +44,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
|
||||
const sessionTabs = useSessionTabs()
|
||||
const toast = useToast()
|
||||
const themes = useThemes()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const mode = themes.mode
|
||||
const paths = useTuiPaths()
|
||||
const dimensions = useTerminalDimensions()
|
||||
|
||||
@@ -17,7 +17,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
|
||||
const client = useClient()
|
||||
const dialog = useDialog()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const [loadError, setLoadError] = createSignal<unknown>()
|
||||
const [showPassword, setShowPassword] = createSignal(false)
|
||||
const [passwordHover, setPasswordHover] = createSignal(false)
|
||||
|
||||
@@ -30,7 +30,7 @@ export function DialogSessionList() {
|
||||
const route = useRoute()
|
||||
const data = useData()
|
||||
const themes = useThemes()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const mode = themes.mode
|
||||
const client = useClient()
|
||||
const local = useLocal()
|
||||
|
||||
@@ -13,7 +13,7 @@ const PAGE_BYTES = 64 * 1024
|
||||
export function DialogShellOutput(props: { shell: ShellInfo; location: LocationRef }) {
|
||||
const client = useClient()
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const dimensions = useTerminalDimensions()
|
||||
const [info, setInfo] = createSignal(props.shell)
|
||||
const [output, setOutput] = createSignal<string>()
|
||||
|
||||
@@ -29,7 +29,7 @@ function getStashPreview(input: string, maxLength: number = 50): string {
|
||||
export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
|
||||
const dialog = useDialog()
|
||||
const stash = usePromptStash()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const shortcuts = Keymap.useShortcuts()
|
||||
|
||||
const [toDelete, setToDelete] = createSignal<number>()
|
||||
|
||||
@@ -6,7 +6,7 @@ import { For, Match, Switch, Show, createMemo } from "solid-js"
|
||||
|
||||
export function DialogStatus() {
|
||||
const data = useData()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const dialog = useDialog()
|
||||
|
||||
const mcp = createMemo(() => data.location.mcp.server.list() ?? [])
|
||||
|
||||
@@ -15,7 +15,7 @@ export function DialogUpdate(props: {
|
||||
restart: () => void
|
||||
}) {
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const [error, setError] = createSignal<string>()
|
||||
const [active, setActive] = createSignal(0)
|
||||
const controller = new AbortController()
|
||||
|
||||
@@ -31,8 +31,8 @@ export function DialogWorkspaceFileChanges(props: {
|
||||
message?: string
|
||||
}) {
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
const overlayTheme = useTheme("overlay")
|
||||
const theme = useTheme().surface("raised")
|
||||
const overlayTheme = useTheme().surface("overlay")
|
||||
const config = useConfig().data
|
||||
const dimensions = useTerminalDimensions()
|
||||
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
|
||||
|
||||
@@ -40,7 +40,7 @@ export function DialogWorkspaces(props: DialogWorkspacesProps) {
|
||||
const dialog = useDialog()
|
||||
const client = useClient()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const sessionData = useData()
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useConfig } from "../config"
|
||||
|
||||
export function DialogWorktreeName(props: { onConfirm: (name: string) => void }) {
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const shortcuts = Keymap.useShortcuts()
|
||||
const config = useConfig().data
|
||||
const [inputTarget, setInputTarget] = createSignal<InputRenderable>()
|
||||
|
||||
@@ -10,7 +10,7 @@ type Progress = { label: string; numerator?: number; denominator?: number }
|
||||
export function MigrationOverlay() {
|
||||
const client = useClient()
|
||||
const toast = useToast()
|
||||
const theme = useTheme("overlay")
|
||||
const theme = useTheme().surface("overlay")
|
||||
const [progress, setProgress] = createSignal<Progress>()
|
||||
const abort = new AbortController()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BoxRenderable } from "@opentui/core"
|
||||
import { onCleanup, onMount } from "solid-js"
|
||||
import { usePanel, type PanelTarget } from "../context/panel"
|
||||
import { InteractivityProvider } from "../context/interactivity"
|
||||
import { ThemeContextProvider, useTheme } from "../context/theme"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { Slot } from "../plugin/render"
|
||||
|
||||
export function PanelHost(props: {
|
||||
@@ -19,6 +19,9 @@ export function PanelHost(props: {
|
||||
|
||||
const Content = () => {
|
||||
const theme = useTheme()
|
||||
// Side panels sit on a raised surface; fullscreen takes over the base background.
|
||||
const background = () =>
|
||||
panels.presentation() === "panel" ? theme.surface("raised").background.default : theme.background.default
|
||||
return (
|
||||
<box
|
||||
id="session-panel"
|
||||
@@ -27,7 +30,7 @@ export function PanelHost(props: {
|
||||
minWidth={0}
|
||||
minHeight={0}
|
||||
focusable
|
||||
backgroundColor={theme.background.default}
|
||||
backgroundColor={background()}
|
||||
onMouseDown={props.onFocus}
|
||||
>
|
||||
<Slot
|
||||
@@ -55,9 +58,7 @@ export function PanelHost(props: {
|
||||
|
||||
return (
|
||||
<InteractivityProvider enabled={props.focused}>
|
||||
<ThemeContextProvider context={panels.presentation() === "panel" ? "elevated" : undefined}>
|
||||
<Content />
|
||||
</ThemeContextProvider>
|
||||
<Content />
|
||||
</InteractivityProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export function Autocomplete(props: {
|
||||
const data = useData()
|
||||
const keymap = Keymap.use()
|
||||
const keymapCommands = Keymap.useCommands()
|
||||
const theme = useTheme("overlay")
|
||||
const theme = useTheme().surface("overlay")
|
||||
const dimensions = useTerminalDimensions()
|
||||
const frecency = useFrecency()
|
||||
const config = useConfig().data
|
||||
|
||||
@@ -350,7 +350,7 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
createEffect(() => {
|
||||
if (!input || input.isDestroyed) return
|
||||
input.cursorColor = disabled() ? theme.background.surface.offset : theme.text.default
|
||||
input.cursorColor = disabled() ? theme.background.raised.base : theme.text.default
|
||||
if (config.cursor) input.cursorStyle = config.cursor
|
||||
})
|
||||
|
||||
@@ -1114,7 +1114,17 @@ export function Prompt(props: PromptProps) {
|
||||
if (!trimmed && (!props.sessionID || store.mode === "shell" || delivery === "queue"))
|
||||
return delivery === "steer" ? (await props.onEmptySubmit?.()) === true : false
|
||||
const exitWord = trimmed === "exit" || trimmed === "quit" || trimmed === ":q"
|
||||
const slash = argumentSlash(store.prompt.text, keymapCommands())
|
||||
const inputText = expandTrackedPastedText(
|
||||
store.prompt.text,
|
||||
input.extmarks.getAllForTypeId(promptPartTypeId).flatMap((extmark) => {
|
||||
const ref = store.extmarkToPart.get(extmark.id)
|
||||
if (ref?.type !== "pasted") return []
|
||||
const part = store.prompt.pasted[ref.index]
|
||||
if (!part) return []
|
||||
return [{ start: extmark.start, end: extmark.end, text: part.text }]
|
||||
}),
|
||||
)
|
||||
const slash = argumentSlash(inputText, keymapCommands())
|
||||
if (delivery === "queue" && (store.mode === "shell" || exitWord || slash)) {
|
||||
toast.show({ message: "This prompt cannot be queued", variant: "warning" })
|
||||
return false
|
||||
@@ -1128,16 +1138,6 @@ export function Prompt(props: PromptProps) {
|
||||
await slash.command.run(slash.input)
|
||||
return true
|
||||
}
|
||||
const inputText = expandTrackedPastedText(
|
||||
store.prompt.text,
|
||||
input.extmarks.getAllForTypeId(promptPartTypeId).flatMap((extmark) => {
|
||||
const ref = store.extmarkToPart.get(extmark.id)
|
||||
if (ref?.type !== "pasted") return []
|
||||
const part = store.prompt.pasted[ref.index]
|
||||
if (!part) return []
|
||||
return [{ start: extmark.start, end: extmark.end, text: part.text }]
|
||||
}),
|
||||
)
|
||||
const slashHead = parseSlashHead(inputText, /\s/)
|
||||
const isCommand =
|
||||
slashHead !== undefined &&
|
||||
@@ -1642,7 +1642,7 @@ export function Prompt(props: PromptProps) {
|
||||
})
|
||||
const maxHeight = createMemo(() => Math.max(6, Math.floor(dimensions().height / 3)))
|
||||
|
||||
const promptBg = createMemo(() => theme.raise(theme.background.surface.offset))
|
||||
const promptBg = createMemo(() => theme.raise(theme.background.raised.base))
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1799,7 +1799,7 @@ export function Prompt(props: PromptProps) {
|
||||
setTimeout(() => {
|
||||
// setTimeout is a workaround and needs to be addressed properly
|
||||
if (!input || input.isDestroyed) return
|
||||
input.cursorColor = disabled() ? theme.background.surface.offset : theme.text.default
|
||||
input.cursorColor = disabled() ? theme.background.raised.base : theme.text.default
|
||||
if (config.cursor) input.cursorStyle = config.cursor
|
||||
}, 0)
|
||||
}}
|
||||
@@ -1818,7 +1818,7 @@ export function Prompt(props: PromptProps) {
|
||||
r.stopPropagation()
|
||||
}}
|
||||
focusedBackgroundColor="transparent"
|
||||
cursorColor={disabled() ? theme.background.surface.offset : theme.text.default}
|
||||
cursorColor={disabled() ? theme.background.raised.base : theme.text.default}
|
||||
syntaxStyle={syntax()}
|
||||
/>
|
||||
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1} justifyContent="space-between">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTheme } from "../context/theme"
|
||||
import { Spinner } from "./spinner"
|
||||
|
||||
export function Reconnecting(props: { managed?: boolean }) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
|
||||
return (
|
||||
<box
|
||||
|
||||
@@ -10,7 +10,7 @@ export function SessionTabsRailControls(props: {
|
||||
tabs: SessionTabsController
|
||||
belowHighlighted: boolean
|
||||
}) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const keymap = Keymap.use()
|
||||
const [hovered, setHovered] = createSignal(false)
|
||||
const hoverColor = createMemo(() =>
|
||||
|
||||
@@ -366,7 +366,7 @@ export function createTabMarquee(animations: () => boolean) {
|
||||
|
||||
function TabContextMenu(props: { state: TabContextMenuState; tabs: SessionTabsController; onClose: () => void }) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const dialog = useDialog()
|
||||
onCleanup(Keymap.use().mode.push("menu"))
|
||||
Keymap.createLayer(() => ({
|
||||
@@ -515,7 +515,7 @@ function VerticalSessionTabs(props: {
|
||||
const data = props.controller ? undefined : useData()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const renderer = useRenderer()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const base = useTheme()
|
||||
const config = useConfig().data
|
||||
const animations = () => props.animations ?? config.animations ?? true
|
||||
@@ -1530,7 +1530,7 @@ function HorizontalSessionTabs(props: {
|
||||
const lifted = (hovered() === tab.sessionID || dragged()) && !selected()
|
||||
const base = lifted ? theme.background.action.primary.hovered : theme.background.default
|
||||
// A dragged tab lifts to full selected elevation while it is held.
|
||||
return tint(base, theme.raise(theme.background.surface.offset), dragged() ? 1 : selection())
|
||||
return tint(base, theme.raise(theme.background.raised.base), dragged() ? 1 : selection())
|
||||
})
|
||||
const pulseColor = () => tint(background(), theme.text.default, 0.45)
|
||||
// The edge flash washes toward a brighter stop on the same background-to-text ramp,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTheme } from "../context/theme"
|
||||
import { Spinner } from "./spinner"
|
||||
|
||||
export function StartupLoading(props: { ready: () => boolean }) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const [show, setShow] = createSignal(false)
|
||||
const text = createMemo(() => (props.ready() ? "Finishing startup…" : "Loading plugins…"))
|
||||
let wait: NodeJS.Timeout | undefined
|
||||
|
||||
@@ -32,7 +32,7 @@ export function TerminalPane(props: {
|
||||
const client = useClient()
|
||||
const keymap = Keymap.use()
|
||||
const leader = Keymap.useLeaderActive()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const themes = useThemes()
|
||||
const renderer = useRenderer()
|
||||
const [failure, setFailure] = createSignal<string>()
|
||||
@@ -155,7 +155,7 @@ export function TerminalPane(props: {
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const tokens = themes.currentTokens().contextual.elevated
|
||||
const tokens = themes.currentTokens().surface("raised")
|
||||
terminalTheme = terminalPalette(tokens, themes.mode(), tokens.background.default)
|
||||
applyTerminalTheme()
|
||||
})
|
||||
@@ -284,7 +284,7 @@ export function TerminalPane(props: {
|
||||
minWidth={0}
|
||||
minHeight={0}
|
||||
overflow="hidden"
|
||||
backgroundColor={themes.currentTokens().contextual.elevated.background.default}
|
||||
backgroundColor={themes.currentTokens().surface("raised").background.default}
|
||||
onSizeChange={function () {
|
||||
size = { cols: Math.max(1, this.width - 2), rows: this.height }
|
||||
if (controller && restored) interact()
|
||||
|
||||
@@ -158,7 +158,7 @@ export const Info = Schema.Struct({
|
||||
description: "Show user attachment and tool-result images in the session transcript",
|
||||
}),
|
||||
tps: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "Show output tokens per second in assistant footers",
|
||||
description: "Show average tokens per second",
|
||||
}),
|
||||
markdown: Schema.optional(Schema.Literals(["source", "rendered"])).annotate({
|
||||
description: "Show Markdown syntax markers or conceal them in rendered transcript content",
|
||||
|
||||
@@ -126,6 +126,7 @@ export const Definitions = {
|
||||
"session.interrupt": keybind("escape", "Interrupt current session"),
|
||||
"session.background": keybind("ctrl+b", "Background blocking session tools"),
|
||||
"session.compact": keybind("<leader>c", "Compact the session"),
|
||||
"session.aside": keybind("none", "Ask a side question"),
|
||||
"session.cd": keybind("none", "Change working directory"),
|
||||
"session.queued_prompts": keybind("<leader>q", "Manage queued prompts"),
|
||||
"queued_prompt.delete": keybind("ctrl+d", "Delete queued prompt"),
|
||||
|
||||
@@ -11,8 +11,10 @@ import { readJson, writeJsonAtomic } from "../util/persistence"
|
||||
import {
|
||||
createModelPreferenceRepository,
|
||||
cycleModelVariant,
|
||||
favoriteModels,
|
||||
modelPreferenceKey,
|
||||
normalizeModelVariant,
|
||||
recentModels,
|
||||
type ModelPreference,
|
||||
type ModelPreferenceModel,
|
||||
} from "../model-preference"
|
||||
@@ -24,19 +26,6 @@ import { usePermission } from "./permission"
|
||||
import { useLocation } from "./location"
|
||||
import { parse } from "../util/model"
|
||||
|
||||
export function recentModels(model: ModelPreferenceModel, recent: ModelPreferenceModel[]) {
|
||||
const seen = new Set<string>()
|
||||
return [model, ...recent]
|
||||
.filter((item) => {
|
||||
const key = modelPreferenceKey(item)
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
.slice(0, 10)
|
||||
.map((item) => ({ providerID: item.providerID, modelID: item.modelID }))
|
||||
}
|
||||
|
||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
name: "Local",
|
||||
init: () => {
|
||||
@@ -167,37 +156,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const pendingSelectionCommits = new Map<string, { agentID: string; selection: string }>()
|
||||
const selectionKey = (value: ModelSelection) =>
|
||||
`${modelPreferenceKey(value)}:${normalizeModelVariant(value.variant) ?? "default"}`
|
||||
const saveState = {
|
||||
pending: false,
|
||||
}
|
||||
|
||||
function savePreferences() {
|
||||
if (!preferences.ready) {
|
||||
saveState.pending = true
|
||||
return
|
||||
}
|
||||
saveState.pending = false
|
||||
void repository
|
||||
.patch({
|
||||
recent: preferences.recent,
|
||||
favorite: preferences.favorite,
|
||||
variant: preferences.variant,
|
||||
})
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
repository
|
||||
.load()
|
||||
.then((value) => {
|
||||
function applyPreferences(value: ModelPreference) {
|
||||
batch(() => {
|
||||
setPreferences("recent", value.recent)
|
||||
setPreferences("favorite", value.favorite)
|
||||
setPreferences("variant", value.variant)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
setPreferences("ready", true)
|
||||
if (saveState.pending) savePreferences()
|
||||
})
|
||||
}
|
||||
|
||||
onCleanup(repository.subscribe(applyPreferences))
|
||||
|
||||
const configuredModel = createMemo(() => {
|
||||
const entry = data.location.config
|
||||
@@ -479,7 +448,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
if (!next) return
|
||||
if (!selectModel({ ...next })) return
|
||||
setPreferences("recent", recentModels(next, preferences.recent))
|
||||
savePreferences()
|
||||
void repository.addRecent(next).catch(() => undefined)
|
||||
},
|
||||
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
|
||||
batch(() => {
|
||||
@@ -487,7 +456,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
if (!selectModel(model)) return
|
||||
if (options?.recent) {
|
||||
setPreferences("recent", recentModels(model, preferences.recent))
|
||||
savePreferences()
|
||||
void repository.addRecent(model).catch(() => undefined)
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -497,14 +466,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const exists = preferences.favorite.some(
|
||||
(x) => x.providerID === model.providerID && x.modelID === model.modelID,
|
||||
)
|
||||
const next = exists
|
||||
? preferences.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID)
|
||||
: [model, ...preferences.favorite]
|
||||
setPreferences(
|
||||
"favorite",
|
||||
next.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
|
||||
)
|
||||
savePreferences()
|
||||
setPreferences("favorite", favoriteModels(model, preferences.favorite, !exists))
|
||||
void repository.setFavorite(model, !exists).catch(() => undefined)
|
||||
})
|
||||
},
|
||||
variant: {
|
||||
@@ -527,7 +490,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
setSessionDraft(route.data.sessionID, { ...m, variant: normalizeModelVariant(value) })
|
||||
}
|
||||
setPreferences("variant", modelPreferenceKey(m), value ?? "default")
|
||||
savePreferences()
|
||||
void repository.saveVariant(m, value).catch(() => undefined)
|
||||
},
|
||||
cycle() {
|
||||
const variants = this.list()
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
resolveThemeDocument,
|
||||
themeModes,
|
||||
type ResolvedTheme,
|
||||
type ContextName,
|
||||
} from "@opencode/theme/tui"
|
||||
import {
|
||||
DEFAULT_THEMES,
|
||||
@@ -23,8 +22,8 @@ import {
|
||||
} from "../theme"
|
||||
import { generateSystem, terminalMode } from "../theme/system"
|
||||
import { discoverThemes } from "../theme/discovery"
|
||||
import { createComponentTheme, createComponentThemeView, type ComponentTheme } from "../theme/component"
|
||||
import { createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor, type ParentProps } from "solid-js"
|
||||
import { createComponentTheme, type ComponentTheme } from "../theme/component"
|
||||
import { createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useConfig } from "../config"
|
||||
@@ -122,7 +121,6 @@ type Themes = {
|
||||
}
|
||||
|
||||
type ThemeContextValue = {
|
||||
current: ComponentTheme["contextual"][ContextName]
|
||||
themes: Themes
|
||||
readonly ready: boolean
|
||||
}
|
||||
@@ -322,7 +320,7 @@ const themeContext = createSimpleContext({
|
||||
const tokens = () => selected().theme
|
||||
tokens()
|
||||
themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`)
|
||||
const current = createComponentTheme(tokens, mode)
|
||||
const current = createComponentTheme(tokens)
|
||||
|
||||
createEffect(() => renderer.setBackgroundColor(tokens().background.default))
|
||||
|
||||
@@ -365,7 +363,6 @@ const themeContext = createSimpleContext({
|
||||
},
|
||||
}
|
||||
return {
|
||||
current,
|
||||
themes: service,
|
||||
get ready() {
|
||||
return service.ready
|
||||
@@ -377,11 +374,8 @@ const themeContext = createSimpleContext({
|
||||
export function useThemes() {
|
||||
return themeContext.use().themes
|
||||
}
|
||||
export function useTheme(): ComponentTheme
|
||||
export function useTheme(context: ContextName): ComponentTheme["contextual"][ContextName]
|
||||
export function useTheme(context?: ContextName) {
|
||||
const value = themeContext.use()
|
||||
return context ? value.themes.current.contextual[context] : value.current
|
||||
export function useTheme(): ComponentTheme {
|
||||
return themeContext.use().themes.current
|
||||
}
|
||||
export const ThemeProvider = themeContext.provider
|
||||
|
||||
@@ -391,20 +385,6 @@ function usablePalette(colors: TerminalColors | undefined): colors is TerminalCo
|
||||
)
|
||||
}
|
||||
|
||||
/** Switches context without remounting children; undefined inherits the enclosing view. */
|
||||
export function ThemeContextProvider(props: ParentProps<{ context: ContextName | undefined }>) {
|
||||
const value = themeContext.use()
|
||||
const current = createComponentThemeView(() => {
|
||||
const name = props.context
|
||||
return name ? value.themes.currentTokens().contextual[name] : value.current
|
||||
}, value.themes.mode)
|
||||
return (
|
||||
<themeContext.context.Provider value={{ current, themes: value.themes, ready: value.ready }}>
|
||||
{props.children}
|
||||
</themeContext.context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function loadTheme(source: ThemeDocumentSource, name: string, requested: "dark" | "light") {
|
||||
const document = parseTheme(source, name)
|
||||
const modes = themeModes(document)
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { Plugin } from "@opencode/plugin/tui"
|
||||
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
|
||||
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
||||
import { createMemo, createSignal, Show } from "solid-js"
|
||||
import { Spinner } from "../../component/spinner"
|
||||
import { useConfig } from "../../config"
|
||||
import { useClipboard } from "../../context/clipboard"
|
||||
import { Keymap } from "../../context/keymap"
|
||||
import { useTheme, useThemes } from "../../context/theme"
|
||||
import { usePlugin } from "../../plugin/context"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import { getScrollAcceleration } from "../../util/scroll"
|
||||
|
||||
// session.generate exposes the session's tools but runs no tool loop, so a
|
||||
// tool call would surface as an empty answer.
|
||||
const instructions = [
|
||||
"The user is asking a quick side question about the conversation so far.",
|
||||
"Answer directly and concisely in markdown from what you already know.",
|
||||
"Do not call any tools and do not take any actions.",
|
||||
].join(" ")
|
||||
|
||||
export default Plugin.define({
|
||||
id: "opencode.btw",
|
||||
setup(context) {
|
||||
const [pending, setPending] = createSignal(0)
|
||||
|
||||
context.ui.slot({
|
||||
append: "prompt.footer.status",
|
||||
render: () => {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<Show when={pending() > 0}>
|
||||
<box flexShrink={0}>
|
||||
<Spinner color={theme.text.status.running}>/btw</Spinner>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
context.ui.slot({
|
||||
append: "app",
|
||||
render() {
|
||||
const toast = useToast()
|
||||
context.keymap.layer(() => ({
|
||||
mode: "global",
|
||||
commands: [
|
||||
{
|
||||
id: "session.aside",
|
||||
title: "Ask a side question",
|
||||
description: "One-shot answer from the session's context without adding to the conversation",
|
||||
group: "Session",
|
||||
palette: true,
|
||||
slash: { name: "btw", arguments: true },
|
||||
async run(input) {
|
||||
const route = context.ui.router.current()
|
||||
if (route.type !== "session") {
|
||||
toast.show({ message: "Open a session first", variant: "warning" })
|
||||
return
|
||||
}
|
||||
const question =
|
||||
input?.trim() || (await context.ui.dialog.prompt({ title: "/btw", placeholder: "Ask anything" }))
|
||||
if (!question) return
|
||||
setPending((count) => count + 1)
|
||||
await context.client.session
|
||||
.generate({ sessionID: route.sessionID, prompt: [instructions, question].join("\n\n") })
|
||||
.then((result) => {
|
||||
context.ui.dialog.show(() => <Answer question={question} answer={result.text.trim()} />)
|
||||
context.ui.dialog.set({ size: "large", centered: true })
|
||||
})
|
||||
.catch((cause: unknown) => toast.error(cause))
|
||||
.finally(() => setPending((count) => count - 1))
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
return null
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
function Answer(props: { question: string; answer: string }) {
|
||||
const dialog = useDialog()
|
||||
const toast = useToast()
|
||||
const clipboard = useClipboard()
|
||||
const plugins = usePlugin()
|
||||
const theme = useTheme().surface("raised")
|
||||
const overlay = useTheme().surface("overlay")
|
||||
const syntax = useThemes().currentSyntax
|
||||
const config = useConfig().data
|
||||
const dimensions = useTerminalDimensions()
|
||||
const maxHeight = createMemo(() => Math.max(3, Math.floor(dimensions().height / 2)))
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
let scroll: ScrollBoxRenderable | undefined
|
||||
|
||||
const copy = () => {
|
||||
void clipboard
|
||||
.write(props.answer)
|
||||
.then(() => setCopied(true))
|
||||
.catch(toast.error)
|
||||
}
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
mode: "modal",
|
||||
commands: [{ bind: "c", title: "Copy answer", group: "Dialog", run: copy }],
|
||||
}))
|
||||
|
||||
useKeyboard((event) => {
|
||||
if (!scroll) return
|
||||
if (event.name === "up") return scroll.scrollBy(-1)
|
||||
if (event.name === "down") return scroll.scrollBy(1)
|
||||
if (event.name === "pageup") return scroll.scrollBy(-maxHeight())
|
||||
if (event.name === "pagedown") return scroll.scrollBy(maxHeight())
|
||||
if (event.name === "home") return scroll.scrollTo(0)
|
||||
if (event.name === "end") return scroll.scrollTo(scroll.scrollHeight)
|
||||
})
|
||||
|
||||
return (
|
||||
<box gap={1}>
|
||||
<box paddingLeft={2} paddingRight={2}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
|
||||
/btw
|
||||
</text>
|
||||
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<text fg={theme.text.subdued} wrapMode="word">
|
||||
{props.question}
|
||||
</text>
|
||||
</box>
|
||||
<scrollbox
|
||||
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
||||
maxHeight={maxHeight()}
|
||||
backgroundColor={overlay.background.default}
|
||||
scrollbarOptions={{ visible: false }}
|
||||
scrollAcceleration={getScrollAcceleration(config)}
|
||||
>
|
||||
<box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
|
||||
<markdown
|
||||
syntaxStyle={syntax()}
|
||||
renderNode={plugins.markdown()}
|
||||
content={props.answer}
|
||||
conceal
|
||||
internalBlockMode="top-level"
|
||||
tableOptions={{ style: "grid", cellPaddingX: 1 }}
|
||||
fg={overlay.markdown.text}
|
||||
bg={overlay.background.default}
|
||||
/>
|
||||
</box>
|
||||
</scrollbox>
|
||||
<box flexDirection="row" gap={3} paddingLeft={2} paddingRight={2} paddingBottom={1}>
|
||||
<text onMouseUp={copy}>
|
||||
<span style={{ fg: copied() ? theme.text.feedback.success.default : theme.text.default }}>
|
||||
<b>{copied() ? "✓ copied" : "c"}</b>
|
||||
</span>
|
||||
<span style={{ fg: theme.text.subdued }}>{copied() ? "" : " copy"}</span>
|
||||
</text>
|
||||
<text fg={theme.text.subdued}>↑/↓ scroll</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export function DiffFileMenu(props: {
|
||||
onClose: () => void
|
||||
}) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const theme = props.context.theme.contextual.overlay
|
||||
const theme = props.context.theme.surface("overlay")
|
||||
const [hovered, setHovered] = createSignal(false)
|
||||
const label = () => (props.reviewed ? "Mark incomplete" : "Mark complete")
|
||||
const width = () => Math.min(19, dimensions().width)
|
||||
|
||||
@@ -27,7 +27,7 @@ export type DiffViewerFileTreeProps = {
|
||||
}
|
||||
|
||||
export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const [sourceHovered, setSourceHovered] = createSignal(false)
|
||||
const list = () => props.layout === "list"
|
||||
const tree = createMemo(() => buildFileTree(props.files))
|
||||
|
||||
@@ -214,7 +214,7 @@ function DiffBaseDialog(props: {
|
||||
current?: string
|
||||
onSelect: (ref: string) => void
|
||||
}) {
|
||||
const theme = props.context.theme.contextual.elevated
|
||||
const theme = props.context.theme.surface("raised")
|
||||
const [search, setSearch] = createDebouncedSignal("", 150)
|
||||
const [branches] = createResource(search, (search) =>
|
||||
props.context.client.vcs.branch.list({ location: props.location, search, limit: 100 }),
|
||||
@@ -887,7 +887,7 @@ export function DiffViewerContent(props: {
|
||||
)
|
||||
edge.backgroundColor =
|
||||
entry && reviewedFileNames().has(entry.file.file)
|
||||
? theme.background.surface.overlay
|
||||
? theme.background.raised.high
|
||||
: theme.diff.background.context
|
||||
}
|
||||
renderer.registerLifecyclePass(edge)
|
||||
@@ -911,7 +911,7 @@ export function DiffViewerContent(props: {
|
||||
{(entry, index) => {
|
||||
const reviewed = () => reviewedFileNames().has(entry.file.file)
|
||||
const background = () =>
|
||||
reviewed() ? theme.background.surface.overlay : theme.diff.background.context
|
||||
reviewed() ? theme.background.raised.high : theme.diff.background.context
|
||||
const image = () => isDiffImageFile(entry.file.file)
|
||||
const countsWidth = () =>
|
||||
(image() ? 6 : String(entry.file.additions).length + String(entry.file.deletions).length + 5) +
|
||||
@@ -1079,7 +1079,7 @@ export function DiffViewerContent(props: {
|
||||
|
||||
function DiffViewerHelpDialog(props: { context: Plugin.Context; single: boolean }) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const theme = props.context.theme.contextual.elevated
|
||||
const theme = props.context.theme.surface("raised")
|
||||
const shortcut =
|
||||
(...ids: string[]) =>
|
||||
() =>
|
||||
|
||||
@@ -14,7 +14,7 @@ export function StoryFooter(props: {
|
||||
message?: string
|
||||
controls: readonly StoryFooterControl[]
|
||||
}) {
|
||||
const theme = props.context.theme.contextual.elevated
|
||||
const theme = props.context.theme.surface("raised")
|
||||
|
||||
return (
|
||||
<box flexShrink={0} flexDirection="column" backgroundColor={theme.background.default}>
|
||||
|
||||
@@ -11,7 +11,7 @@ const directory = "/Users/kit/code/open-source/opencode-workerd-profile"
|
||||
|
||||
function SessionLocationMissingStory(props: { context: Plugin.Context }) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const theme = props.context.theme.contextual.elevated
|
||||
const theme = props.context.theme.surface("raised")
|
||||
const [message, setMessage] = createSignal("Choose another directory to continue")
|
||||
const open = () =>
|
||||
props.context.ui.dialog.show(() => (
|
||||
|
||||
@@ -115,7 +115,7 @@ function map(
|
||||
syntax?: SyntaxStyle,
|
||||
system = false,
|
||||
): RunTheme {
|
||||
const elevated = theme.contextual.elevated
|
||||
const elevated = theme.surface("raised")
|
||||
// V1 system migration serializes colors; restore terminal defaults before quantizing scrollback.
|
||||
const exact = (color: RGBA) => {
|
||||
if (system && color.equals(theme.text.default)) return RGBA.defaultForeground(color)
|
||||
@@ -159,9 +159,9 @@ function map(
|
||||
text: exact(theme.text.default),
|
||||
shade: exact(elevated.background.default),
|
||||
surface: exact(elevated.background.default),
|
||||
pane: exact(theme.contextual.overlay.background.default),
|
||||
pane: exact(theme.surface("overlay").background.default),
|
||||
border: exact(theme.border.default),
|
||||
line: exact(theme.background.surface.overlay),
|
||||
line: exact(theme.background.raised.high),
|
||||
},
|
||||
entry: {
|
||||
system: { body: scrollback(theme.text.subdued) },
|
||||
@@ -174,7 +174,7 @@ function map(
|
||||
splash: {
|
||||
left: nearestIndexed(indexed, theme.text.subdued),
|
||||
right: nearestIndexed(indexed, theme.text.default),
|
||||
leftShadow: nearestIndexed(indexed, theme.background.surface.offset),
|
||||
leftShadow: nearestIndexed(indexed, theme.background.raised.base),
|
||||
},
|
||||
block: {
|
||||
text: scrollback(theme.text.default),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { readJson, writeJsonAtomic } from "./util/persistence"
|
||||
import { isRecord } from "./util/record"
|
||||
import { Flock } from "@opencode/util/flock"
|
||||
import { watch } from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
export type ModelPreferenceModel = {
|
||||
providerID: string
|
||||
@@ -43,6 +46,24 @@ export function modelPreferenceKey(model: ModelPreferenceModel) {
|
||||
return `${model.providerID}/${model.modelID}`
|
||||
}
|
||||
|
||||
export function recentModels(model: ModelPreferenceModel, recent: ModelPreferenceModel[]) {
|
||||
const seen = new Set<string>()
|
||||
return [model, ...recent]
|
||||
.filter((item) => {
|
||||
const key = modelPreferenceKey(item)
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
.slice(0, 10)
|
||||
.map((item) => ({ providerID: item.providerID, modelID: item.modelID }))
|
||||
}
|
||||
|
||||
export function favoriteModels(model: ModelPreferenceModel, favorite: ModelPreferenceModel[], enabled: boolean) {
|
||||
const current = favorite.filter((item) => modelPreferenceKey(item) !== modelPreferenceKey(model))
|
||||
return enabled ? [model, ...current] : current
|
||||
}
|
||||
|
||||
export function cycleModelVariant(current: string | undefined, variants: string[]) {
|
||||
const named = variants.filter((variant) => variant !== "default")
|
||||
if (named.length === 0) return undefined
|
||||
@@ -80,32 +101,76 @@ function patch(value: Partial<ModelPreference>) {
|
||||
}
|
||||
|
||||
export function createModelPreferenceRepository(filePath: string) {
|
||||
const state = {
|
||||
pending: Promise.resolve(),
|
||||
}
|
||||
let pending = Promise.resolve()
|
||||
let revision = 0
|
||||
let watcher: ReturnType<typeof watch> | undefined
|
||||
let reload: ReturnType<typeof setTimeout> | undefined
|
||||
const listeners = new Set<(value: ModelPreference) => void>()
|
||||
const read = () =>
|
||||
readJson<unknown>(filePath)
|
||||
.then(decodeModelPreference)
|
||||
.catch(() => decodeModelPreference(undefined))
|
||||
|
||||
function update(change: (current: ModelPreference) => Partial<ModelPreference>) {
|
||||
const result = state.pending.then(async () => {
|
||||
const current = await read()
|
||||
const next = { ...current, ...patch(change(preference(current))) }
|
||||
await writeJsonAtomic(filePath, next)
|
||||
})
|
||||
state.pending = result.catch(() => undefined)
|
||||
const result = pending.then(() =>
|
||||
Flock.withLock(
|
||||
filePath,
|
||||
async () => {
|
||||
const current = await read()
|
||||
await writeJsonAtomic(filePath, { ...current, ...patch(change(preference(current))) })
|
||||
},
|
||||
{ dir: path.join(path.dirname(filePath), "locks") },
|
||||
),
|
||||
)
|
||||
pending = result.then(
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
function load() {
|
||||
return state.pending.then(read).then(preference)
|
||||
return pending.then(read).then(preference)
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const current = ++revision
|
||||
const value = await load()
|
||||
if (current !== revision) return
|
||||
listeners.forEach((listener) => listener(value))
|
||||
}
|
||||
|
||||
return {
|
||||
load,
|
||||
patch(value: Partial<ModelPreference>) {
|
||||
return update(() => value)
|
||||
addRecent(model: ModelPreferenceModel) {
|
||||
return update((current) => ({ recent: recentModels(model, current.recent) }))
|
||||
},
|
||||
setFavorite(model: ModelPreferenceModel, enabled: boolean) {
|
||||
return update((current) => ({ favorite: favoriteModels(model, current.favorite, enabled) }))
|
||||
},
|
||||
subscribe(listener: (value: ModelPreference) => void) {
|
||||
listeners.add(listener)
|
||||
void refresh()
|
||||
if (!watcher) {
|
||||
watcher = watch(path.dirname(filePath), (_event, filename) => {
|
||||
const changed = filename?.toString()
|
||||
const name = path.basename(filePath)
|
||||
if (changed !== undefined && changed !== name && !changed.startsWith(name + ".")) return
|
||||
clearTimeout(reload)
|
||||
reload = setTimeout(() => void refresh(), 50)
|
||||
})
|
||||
watcher.on("error", () => {
|
||||
watcher?.close()
|
||||
watcher = undefined
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
listeners.delete(listener)
|
||||
if (listeners.size > 0) return
|
||||
clearTimeout(reload)
|
||||
watcher?.close()
|
||||
watcher = undefined
|
||||
}
|
||||
},
|
||||
async resolveVariant(model: ModelPreferenceModel) {
|
||||
return normalizeModelVariant((await load()).variant[modelPreferenceKey(model)])
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import HomeFooter from "../feature-plugins/home/footer"
|
||||
import PromptBtw from "../feature-plugins/prompt/btw"
|
||||
import PromptFooter from "../feature-plugins/prompt/footer"
|
||||
import SidebarContext from "../feature-plugins/sidebar/context"
|
||||
import SidebarFooter from "../feature-plugins/sidebar/footer"
|
||||
@@ -14,6 +15,7 @@ import Merman from "@opencode/merman/plugin"
|
||||
export const builtins = [
|
||||
HomeFooter,
|
||||
PromptFooter,
|
||||
PromptBtw,
|
||||
SidebarContext,
|
||||
SidebarMcp,
|
||||
SidebarFooter,
|
||||
|
||||
@@ -42,7 +42,7 @@ export type ComposerProps = {
|
||||
}
|
||||
|
||||
export function Composer(props: ComposerProps) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const config = useConfig().data
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
|
||||
@@ -21,7 +21,7 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
|
||||
const dialog = useDialog()
|
||||
const clipboard = useClipboard()
|
||||
const toast = useToast()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const dimensions = useTerminalDimensions()
|
||||
const config = useConfig().data
|
||||
const [copied, setCopied] = createSignal<"code" | "output">()
|
||||
@@ -224,7 +224,7 @@ function GutteredCode(props: {
|
||||
digits: number
|
||||
blocks: Set<CodeRenderable>
|
||||
}) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const syntax = useThemes().currentSyntax
|
||||
const gutter = createMemo(() =>
|
||||
props.content
|
||||
|
||||
@@ -59,7 +59,7 @@ const drafts = new Map<string, FormDraft>()
|
||||
export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
const data = useData()
|
||||
const themes = useThemes()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const themeMode = themes.mode
|
||||
const renderer = useRenderer()
|
||||
const dimensions = useTerminalDimensions()
|
||||
|
||||
@@ -23,7 +23,7 @@ import { SplitBorder } from "../../ui/border"
|
||||
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
|
||||
import { Spinner, SPINNER_FRAMES } from "../../component/spinner"
|
||||
import { PatchDiff } from "../../component/patch-diff"
|
||||
import { createSyntaxStyleMemo, ThemeContextProvider, useTheme, useThemes } from "../../context/theme"
|
||||
import { createSyntaxStyleMemo, useTheme, useThemes } from "../../context/theme"
|
||||
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA, MouseEvent } from "@opentui/core"
|
||||
import { Prompt, type PromptRef } from "../../component/prompt"
|
||||
import type {
|
||||
@@ -1292,7 +1292,7 @@ export function Session(props: {
|
||||
paddingLeft: 1,
|
||||
visible: showScrollbar(),
|
||||
trackOptions: {
|
||||
backgroundColor: theme.raise(theme.background.surface.offset),
|
||||
backgroundColor: theme.raise(theme.background.raised.base),
|
||||
foregroundColor: theme.border.default,
|
||||
},
|
||||
}}
|
||||
@@ -1836,7 +1836,7 @@ function SessionReasoningGroupView(props: {
|
||||
<box
|
||||
border={["left"]}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.raise(theme.background.surface.offset)}
|
||||
borderColor={theme.raise(theme.background.raised.base)}
|
||||
paddingLeft={1}
|
||||
>
|
||||
<code
|
||||
@@ -1936,7 +1936,7 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
|
||||
const config = useConfig()
|
||||
const data = useData()
|
||||
const local = useLocal()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const model = createMemo(
|
||||
() =>
|
||||
ctx
|
||||
@@ -2165,7 +2165,7 @@ function RevertMessage(props: {
|
||||
}>
|
||||
}) {
|
||||
const ctx = use()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const route = useRouteData("session")
|
||||
const client = useClient()
|
||||
const toast = useToast()
|
||||
@@ -2268,7 +2268,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
||||
),
|
||||
)
|
||||
const themes = useThemes()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const mode = themes.mode
|
||||
const [hover, setHover] = createSignal(false)
|
||||
const color = createMemo(() => local.agent.color(data.session.get(ctx.sessionID)?.agent ?? "build"))
|
||||
@@ -2384,7 +2384,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
||||
}
|
||||
|
||||
function QueuedPromptDock(props: { prompts: { id: string; text: string }[]; onOpen: () => void }) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const [hover, setHover] = createSignal(false)
|
||||
const next = createMemo(() => props.prompts[0]?.text.replaceAll("\n", " "))
|
||||
|
||||
@@ -2744,8 +2744,9 @@ function InlineTool(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function StatusBadge(props: { children: string }) {
|
||||
const theme = useTheme()
|
||||
function StatusBadge(props: { children: string; raised?: boolean }) {
|
||||
const base = useTheme()
|
||||
const theme = props.raised ? base.surface("raised") : base
|
||||
return (
|
||||
<text flexShrink={0} bg={theme.raise(theme.background.default)} fg={theme.text.subdued}>
|
||||
{" "}
|
||||
@@ -2767,16 +2768,8 @@ type BlockToolProps = {
|
||||
}
|
||||
|
||||
function BlockTool(props: BlockToolProps) {
|
||||
const parentTheme = useTheme()
|
||||
return (
|
||||
<ThemeContextProvider context="elevated">
|
||||
<BlockToolContent {...props} borderColor={parentTheme.background.default} />
|
||||
</ThemeContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function BlockToolContent(props: BlockToolProps & { borderColor: RGBA }) {
|
||||
const theme = useTheme()
|
||||
const base = useTheme()
|
||||
const theme = base.surface("raised")
|
||||
const ctx = use()
|
||||
const renderer = useRenderer()
|
||||
const [hover, setHover] = createSignal(false)
|
||||
@@ -2787,13 +2780,14 @@ function BlockToolContent(props: BlockToolProps & { borderColor: RGBA }) {
|
||||
return (
|
||||
<box
|
||||
border={["left"]}
|
||||
flexShrink={0}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
gap={1}
|
||||
backgroundColor={hover() ? theme.raise(theme.background.default) : theme.background.default}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={props.borderColor}
|
||||
borderColor={base.background.default}
|
||||
onMouseOver={() => props.onClick && setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onMouseUp={() => {
|
||||
@@ -3026,7 +3020,7 @@ function ShellDisplay(props: {
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={props.background}>
|
||||
<StatusBadge>Background</StatusBadge>
|
||||
<StatusBadge raised>Background</StatusBadge>
|
||||
</Show>
|
||||
</box>
|
||||
</BlockTool>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function SessionLocationMissing(props: { directory: string; projectID: st
|
||||
|
||||
export function SessionLocationUnavailable(props: { directory: string; onMove: () => void }) {
|
||||
const paths = useTuiPaths()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const directory = createMemo(() => Locale.truncateMiddle(abbreviateHome(props.directory, paths.home), 72))
|
||||
|
||||
return (
|
||||
|
||||
@@ -277,7 +277,7 @@ function RejectPrompt(props: {
|
||||
}) {
|
||||
let input: TextareaRenderable
|
||||
const enabled = useInteractivity()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const config = useConfig().data
|
||||
const dimensions = useTerminalDimensions()
|
||||
const narrow = createMemo(() => dimensions().width < 80)
|
||||
@@ -418,7 +418,7 @@ export function SessionQuestion<const T extends Record<string, string>>(props: {
|
||||
fullscreen?: boolean
|
||||
onSelect: (option: keyof T) => void
|
||||
}) {
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const dimensions = useTerminalDimensions()
|
||||
const keys = Object.keys(props.options) as (keyof T)[]
|
||||
const [store, setStore] = createStore({
|
||||
|
||||
@@ -378,7 +378,7 @@ export function turnTokensPerSecond(
|
||||
step.time.streamed === undefined ? [] : [Math.max(0, step.time.streamed - step.time.created)],
|
||||
)
|
||||
if (steps.length === 0 || durations.length !== steps.length) return
|
||||
const output = steps.reduce((total, step) => total + (step.tokens?.output ?? 0), 0)
|
||||
const output = steps.reduce((total, step) => total + (step.tokens?.output ?? 0) + (step.tokens?.reasoning ?? 0), 0)
|
||||
const duration = durations.reduce((total, value) => total + value, 0)
|
||||
if (output <= 0 || duration <= 0) return
|
||||
// Aggregate before dividing so each step is weighted by its provider-active duration.
|
||||
|
||||
@@ -12,7 +12,7 @@ import { SESSION_SIDEBAR_WIDTH } from "../../ui/layout"
|
||||
|
||||
export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
const data = useData()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const config = useConfig().data
|
||||
const session = createMemo(() => data.session.get(props.sessionID))
|
||||
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import type { RGBA } from "@opentui/core"
|
||||
import type { Accessor } from "solid-js"
|
||||
import type { Mode, ResolvedTheme, ResolvedThemeTokens } from "@opencode/theme/tui"
|
||||
import type { ResolvedTheme, SurfaceName } from "@opencode/theme/tui"
|
||||
|
||||
export function createComponentTheme(current: Accessor<ResolvedTheme>, mode: Accessor<Mode>) {
|
||||
return Object.assign(createComponentThemeView(current, mode), {
|
||||
contextual: {
|
||||
elevated: createComponentThemeView(() => current().contextual.elevated, mode),
|
||||
overlay: createComponentThemeView(() => current().contextual.overlay, mode),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function createComponentThemeView(view: Accessor<ResolvedThemeTokens>, mode: Accessor<Mode>) {
|
||||
export function createComponentTheme(
|
||||
view: Accessor<ResolvedTheme>,
|
||||
// Shared across a theme's surface views so `surface()` stays absolute at the wrapper level too.
|
||||
surfaces = new Map<SurfaceName, ComponentTheme>(),
|
||||
): ComponentTheme {
|
||||
return {
|
||||
get hue() {
|
||||
return view().hue
|
||||
@@ -43,8 +38,15 @@ export function createComponentThemeView(view: Accessor<ResolvedThemeTokens>, mo
|
||||
source: (color: RGBA) => view().source(color),
|
||||
increase: (color: RGBA, amount = 1) => view().increase(color, amount),
|
||||
decrease: (color: RGBA, amount = 1) => view().decrease(color, amount),
|
||||
raise: (color: RGBA) => (mode() === "light" ? view().increase(color) : view().decrease(color)),
|
||||
raise: (color: RGBA) => view().raise(color),
|
||||
surface(name: SurfaceName) {
|
||||
const cached = surfaces.get(name)
|
||||
if (cached) return cached
|
||||
const created = createComponentTheme(() => view().surface(name), surfaces)
|
||||
surfaces.set(name, created)
|
||||
return created
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export type ComponentTheme = ReturnType<typeof createComponentTheme>
|
||||
export type ComponentTheme = ResolvedTheme
|
||||
|
||||
@@ -11,7 +11,7 @@ export type DialogAlertProps = {
|
||||
|
||||
export function DialogAlert(props: DialogAlertProps) {
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
mode: "modal",
|
||||
|
||||
@@ -19,7 +19,7 @@ export type DialogConfirmProps = {
|
||||
|
||||
export function DialogConfirm(props: DialogConfirmProps) {
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const [store, setStore] = createStore({
|
||||
active: "confirm" as "confirm" | "cancel",
|
||||
})
|
||||
|
||||
@@ -23,8 +23,8 @@ type Active = ExportFormat | "thinking" | "tools" | "sanitize" | "copy" | "expor
|
||||
|
||||
export function DialogExportOptions(props: DialogExportOptionsProps) {
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
const overlayTheme = useTheme("overlay")
|
||||
const theme = useTheme().surface("raised")
|
||||
const overlayTheme = useTheme().surface("overlay")
|
||||
const [store, setStore] = createStore({
|
||||
format: "markdown" as ExportFormat,
|
||||
thinking: props.defaultThinking,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useDialog, type DialogContext } from "./dialog"
|
||||
|
||||
export function DialogExportResult(props: { path: string; onClose?: () => void }) {
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
|
||||
const close = () => {
|
||||
props.onClose?.()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useDialog } from "./dialog"
|
||||
|
||||
export function DialogHelp() {
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
const theme = useTheme().surface("raised")
|
||||
const shortcuts = Keymap.useShortcuts()
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user