Compare commits

...
8 Commits
122 changed files with 2062 additions and 1716 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
Per-type constructors live on the type, not as top-level re-exports. Use `Message.system(...)`, `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `LanguageModel.make(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for request-shaped call APIs: `LLM.request`, `LLM.generate`, `LLM.stream`, and `LLM.generateObject`. Use `LLMRequest.update(...)` when deriving canonical request data; do not add a duplicate `LLM.updateRequest(...)` path. Two ways to construct the same thing is one too many.
- Keep provider-defined string enums forward-compatible. Expose known values for autocomplete while accepting future values with `Known | (string & {})`; use `Schema.String` at runtime unless rejecting unknown values is required for correctness.
- Prefer forward compatibility for provider-defined options that OpenCode only passes through. For pass-through string enums, expose known values for autocomplete while accepting future values with `Known | (string & {})`, and accept any string at runtime. Closed literals are appropriate when OpenCode branches on a value, transforms its associated structure, or otherwise cannot correctly handle an unknown variant. New options whose shape or behavior requires implementation remain unsupported until they are handled; do not blindly forward unknown structures.
- Order reasoning-effort values from lowest to highest: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Provider-specific subsets follow the same relative order in types, schemas, option lists, and tests.
## Tests
@@ -83,7 +83,7 @@ const AnthropicCacheControl = Schema.Struct({
type: Schema.tag("ephemeral"),
ttl: Schema.optional(Schema.Literals(["5m", "1h"])),
})
const AnthropicServiceTier = Schema.Literals(["auto", "standard_only"])
const AnthropicServiceTier = knownString<"auto" | "standard_only">()
const AnthropicTextBlock = Schema.Struct({
type: Schema.tag("text"),
@@ -276,7 +276,7 @@ const AnthropicThinkingBlockBinding = Schema.Struct({
})
const AnthropicThinkingFields = {
display: Schema.optional(Schema.Literals(["summarized", "omitted"])),
display: Schema.optional(knownString<"summarized" | "omitted">()),
block_binding: Schema.optional(AnthropicThinkingBlockBinding),
}
const AnthropicThinkingEnabled = Schema.Struct({
@@ -4,6 +4,22 @@ import { Anthropic } from "../../src/providers.js"
const model = Anthropic.provider.model("claude-sonnet-4-5")
LLM.request({ model, prompt: "Hello", providerOptions: { thinking: { type: "adaptive" } } })
LLM.request({
model,
prompt: "Hello",
providerOptions: {
serviceTier: "future-tier",
thinking: { type: "adaptive", display: "future-display" },
},
})
LLM.request({
model,
prompt: "Hello",
providerOptions: {
// @ts-expect-error Anthropic cache TTL values are protocol constraints.
cacheControl: { type: "ephemeral", ttl: "future-ttl" },
},
})
LLM.request({
model,
@@ -215,13 +215,31 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("forwards unknown values for pass-through string enums", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLMRequest.update(request, {
providerOptions: {
service_tier: "future-tier",
thinking: { type: "adaptive", display: "future-display" },
},
}),
)
expect(prepared.body).toMatchObject({
service_tier: "future-tier",
thinking: { type: "adaptive", display: "future-display" },
})
}),
)
it.effect("ignores unknown provider options and rejects malformed known ones", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(LLMRequest.update(request, { providerOptions: { unknownOption: true } }))
const malformed = [
{ service_tier: "fast" },
{ service_tier: 1 },
{ metadata: { user_id: 42 } },
{ cache_control: { type: "ephemeral", ttl: "2h" } },
{ cache_control: { type: "ephemeral", ttl: "future-ttl" } },
{ output_config: { format: { type: "text" } } },
{ thinking: { type: "automatic" } },
]
+4 -4
View File
@@ -36,17 +36,17 @@ export function ErrorOverlay(props: { component: string; error: unknown; onClose
<Dialog centered onClose={props.onClose}>
<box maxHeight={Math.max(1, dimensions().height - 3)} paddingX={2} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between" flexShrink={0}>
<text fg={theme.text.feedback.error.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.feedback.error.base} attributes={TextAttributes.BOLD}>
Error while hot reloading
</text>
<text fg={theme.text.subdued} onMouseUp={props.onClose}>
<text fg={theme.text.muted} onMouseUp={props.onClose}>
esc
</text>
</box>
<text maxHeight={Math.max(1, dimensions().height - 9)} fg={theme.text.default}>
<text maxHeight={Math.max(1, dimensions().height - 9)} fg={theme.text.base}>
{props.error instanceof Error ? props.error.message : String(props.error)}
</text>
<text flexShrink={0} fg={theme.text.subdued}>
<text flexShrink={0} fg={theme.text.muted}>
{props.component} · Fix the component and save to retry.
</text>
</box>
+18
View File
@@ -1,9 +1,27 @@
#!/usr/bin/env bun
import { $ } from "bun"
import path from "node:path"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
await $`rm -rf dist`
await $`bun tsc -p tsconfig.build.json`
const root = path.resolve("src")
const result = await Bun.build({
entrypoints: await Array.fromAsync(new Bun.Glob("**/*.ts").scan({ cwd: root, absolute: true })).then((files) =>
files.filter((file) => !file.endsWith(".d.ts")),
),
root,
outdir: "dist",
target: "node",
format: "esm",
packages: "external",
splitting: true,
naming: {
entry: "[dir]/[name].[ext]",
chunk: "chunks/[name]-[hash].[ext]",
},
})
if (!result.success) throw new AggregateError(result.logs, "Failed to build Client")
+1 -1
View File
@@ -1,7 +1,7 @@
export * as Watcher from "./watcher.js"
// @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper"
import { createWrapper } from "@parcel/watcher/wrapper.js"
import type ParcelWatcher from "@parcel/watcher"
import { FileSystem } from "@opencode/schema/filesystem"
import { makeGlobalNode } from "@opencode/util/effect/app-node"
+102
View File
@@ -0,0 +1,102 @@
export * as GoalPlugin from "./goal.js"
import { define } from "@opencode/plugin/effect/plugin"
import type { Session } from "@opencode/schema/session"
import { Effect, Option, Schema, Stream } from "effect"
const GoalState = Schema.Struct({
goal: Schema.String,
active: Schema.Boolean,
})
const workers = new WeakSet<object>()
export const Plugin = define({
id: "opencode.goal",
effect: Effect.fn(function* (ctx) {
const key = (sessionID: Session.ID) => `session/${sessionID}/goal`
const read = Effect.fn(function* (sessionID: Session.ID) {
return Option.getOrUndefined(Schema.decodeUnknownOption(GoalState)(yield* ctx.storage.get(key(sessionID))))
})
const evaluate = Effect.fn(function* (sessionID: Session.ID) {
const state = yield* read(sessionID)
if (!state?.active) return
const result = yield* ctx.session.generate({
sessionID,
prompt: [
"Evaluate progress toward the goal below using the current session context.",
"Reply with exactly COMPLETE if it is fully complete.",
"Otherwise reply with CONTINUE followed by one concise instruction for the next step.",
`Goal: ${state.goal}`,
].join("\n\n"),
})
const current = yield* read(sessionID)
if (!current?.active || current.goal !== state.goal) return
const evaluation = result.text.trim()
if (/^COMPLETE\b/i.test(evaluation)) {
yield* ctx.session.synthetic({
sessionID,
text: `Goal: ${state.goal}\n\nThe goal has been completed.`,
description: "Goal completed",
delivery: "steer",
resume: false,
})
yield* ctx.storage.set(key(sessionID), { goal: state.goal, active: false })
return
}
yield* ctx.session.synthetic({
sessionID,
text: [
`Goal: ${state.goal}`,
`Next step: ${evaluation.replace(/^CONTINUE\s*/i, "")}`,
"Continue working autonomously until the goal is complete.",
].join("\n\n"),
description: "Goal continuing",
delivery: "steer",
resume: true,
})
})
if (!workers.has(ctx.app)) {
workers.add(ctx.app)
yield* ctx.event.subscribe().pipe(
Stream.mapEffect(
(event) => {
if (event.type !== "session.execution.succeeded") return Effect.void
return evaluate(event.data.sessionID).pipe(
Effect.catch((error) =>
Effect.logError("goal evaluation failed", { sessionID: event.data.sessionID, error }),
),
)
},
{ concurrency: "unbounded", unordered: true },
),
Stream.runDrain,
Effect.forkDetach({ startImmediately: true }),
)
}
yield* ctx.command.transform((draft) => {
draft.add({
name: "goal",
description: "Work autonomously toward a goal",
execute: Effect.fn(function* ({ sessionID, prompt, delivery }) {
const goal = prompt.text.trim()
if (!goal) return yield* Effect.fail(new Error("Usage: /goal <goal>"))
yield* ctx.storage.set(key(sessionID), { goal, active: true })
yield* ctx.session.synthetic({
sessionID,
text: `Goal: ${goal}\n\nContinue until the goal is fully complete. Use tools and make changes as needed.`,
description: `Goal started: ${goal}`,
delivery,
resume: true,
})
}),
})
})
}),
})
+2
View File
@@ -87,6 +87,7 @@ import { AgentPlugin } from "./agent.js"
import BrowserPlugin from "@opencode/plugin-browser"
import { CommandPlugin } from "./command.js"
import { NativeCompactionPlugin } from "./compaction.js"
import { GoalPlugin } from "./goal.js"
import { IdentityPlugin } from "./identity.js"
import { PlanPlugin } from "./plan.js"
import { ModelsDevPlugin } from "./models-dev.js"
@@ -216,6 +217,7 @@ const pre = [
AgentPlugin.Plugin,
PlanPlugin.Plugin,
CommandPlugin.Plugin,
GoalPlugin.Plugin,
SkillPlugin.Plugin,
VcsHgPlugin.Plugin,
ModelsDevPlugin,
+17
View File
@@ -178,4 +178,21 @@ export const firstUserMessage = Effect.fn("SessionHistory.firstUserMessage")(fun
return message?.type === "user" ? message : undefined
})
/** Returns the session's first synthetic message. */
export const firstSyntheticMessage = Effect.fn("SessionHistory.firstSyntheticMessage")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
) {
const row = yield* db
.select()
.from(SessionMessageTable)
.where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "synthetic")))
.orderBy(asc(SessionMessageTable.seq))
.get()
.pipe(Effect.orDie)
if (!row) return undefined
const message = yield* decodeMessageRow(row).pipe(Effect.orElseSucceed(() => undefined))
return message?.type === "synthetic" ? message : undefined
})
export * as SessionHistory from "./history.js"
+6 -5
View File
@@ -101,14 +101,15 @@ export const layer = Layer.effect(
const session = yield* store.get(sessionID)
if (!session) return
const firstUser = yield* SessionHistory.firstUserMessage(db, session.id)
if (!firstUser) return
const firstPrompt = firstUser ?? (yield* SessionHistory.firstSyntheticMessage(db, session.id))
if (!firstPrompt) return
const text = !isUntitled(session)
? yield* store.context(session.id).pipe(
Effect.map((messages) => {
const original = `Original request:\n${firstUser.text.slice(0, MAX_FIRST_MESSAGE_LENGTH)}`
const original = `Original request:\n${firstPrompt.text.slice(0, MAX_FIRST_MESSAGE_LENGTH)}`
const recent = messages
.flatMap((message) => {
if (message.type === "user" && message.id !== firstUser.id) return [`User: ${message.text.trim()}`]
if (message.type === "user" && message.id !== firstPrompt.id) return [`User: ${message.text.trim()}`]
if (message.type !== "assistant") return []
const text = message.content
.flatMap((part) => (part.type === "text" ? [part.text.trim()] : []))
@@ -121,9 +122,9 @@ export const layer = Layer.effect(
const prefix = `${original}\n\nRecent conversation:\n`
return `${prefix}${recent.slice(-(MAX_CONTEXT_LENGTH - prefix.length))}`
}),
Effect.orElseSucceed(() => firstUser.text),
Effect.orElseSucceed(() => firstPrompt.text),
)
: firstUser.text
: firstPrompt.text
const selection = yield* context.selectTitle(session)
if (!selection) return
const title =
+1 -1
View File
@@ -1,7 +1,7 @@
import { expect, test } from "bun:test"
import { Ignore } from "@opencode/core/filesystem/ignore"
// @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper"
import { createWrapper } from "@parcel/watcher/wrapper.js"
test("parcel patterns ignore built-in folders at any depth", async () => {
let ignoreGlobs: string[] = []
+77
View File
@@ -0,0 +1,77 @@
import { describe, expect } from "bun:test"
import type { CommandDefinition } from "@opencode/plugin/effect/command"
import { Event } from "@opencode/schema/event"
import { Session } from "@opencode/schema/session"
import { SessionEvent } from "@opencode/schema/session-event"
import { SessionInbox } from "@opencode/schema/session-inbox"
import { SessionMessage } from "@opencode/schema/session-message"
import { DateTime, Deferred, Effect, PubSub, Stream } from "effect"
import { GoalPlugin } from "@opencode/core/plugin/goal"
import { it } from "../lib/effect"
import { host } from "./host"
const sessionID = Session.ID.make("ses_goal_test")
describe("GoalPlugin.Plugin", () => {
it.effect("continues a goal until evaluation reports completion", () =>
Effect.gen(function* () {
const event: SessionEvent.Execution.Succeeded = {
id: Event.ID.create(),
created: 0,
durable: { aggregateID: sessionID, seq: Event.Seq.make(0), version: Event.Version.make(1) },
type: "session.execution.succeeded",
data: { sessionID },
}
const events = yield* PubSub.unbounded<typeof event>()
const completed = yield* Deferred.make<void>()
const storage = new Map<string, unknown>()
const descriptions = new Array<string>()
let command: CommandDefinition | undefined
yield* GoalPlugin.Plugin.effect(
host({
command: {
list: () => Effect.die("unused command.list"),
reload: () => Effect.die("unused command.reload"),
transform: (callback) => {
callback({ add: (definition) => (command = definition) })
return Effect.succeed({ dispose: Effect.void })
},
},
event: { subscribe: () => Stream.fromPubSub(events) },
storage: {
get: (key) => Effect.succeed(storage.get(key) as never),
set: (key, value) => Effect.sync(() => storage.set(key, value)),
remove: (key) => Effect.sync(() => storage.delete(key)),
scan: () => Effect.die("unused storage.scan"),
},
session: {
generate: () => Effect.succeed({ text: "COMPLETE" }),
synthetic: (input) =>
Effect.gen(function* () {
descriptions.push(input.description ?? "")
if (input.description === "Goal completed") yield* Deferred.succeed(completed, undefined)
return SessionInbox.Synthetic.make({
id: SessionMessage.ID.create(),
sessionID: input.sessionID,
time: { created: DateTime.makeUnsafe(0) },
type: "synthetic",
payload: { text: input.text, description: input.description },
delivery: input.delivery ?? "steer",
})
}),
},
}),
)
yield* Effect.yieldNow
if (!command) return yield* Effect.die("Goal command was not registered")
yield* command.execute({ sessionID, prompt: { text: "Finish the task" }, delivery: "steer" })
yield* PubSub.publish(events, event)
yield* Deferred.await(completed)
expect(descriptions).toEqual(["Goal started: Finish the task", "Goal completed"])
expect(storage.get(`session/${sessionID}/goal`)).toEqual({ goal: "Finish the task", active: false })
}),
)
})
+2 -2
View File
@@ -5,8 +5,8 @@ export default Plugin.define({
id: "opencode.latex",
setup(context) {
const render = createLatexCodeBlockRenderer(context.renderer, () => ({
text: context.theme.text.default,
subdued: context.theme.text.subdued,
text: context.theme.text.base,
subdued: context.theme.text.muted,
}))
context.markdown.registerCodeBlockRenderer("latex", render)
context.markdown.registerCodeBlockRenderer("math", render)
+6 -6
View File
@@ -83,15 +83,15 @@ describe("OpenCode diagram palette", () => {
}
const theme = {
text: {
default: rgb([230, 232, 240]),
subdued: rgb([114, 120, 138]),
base: rgb([230, 232, 240]),
muted: rgb([114, 120, 138]),
feedback: {
info: { default: rgb([40, 120, 220]) },
success: { default: rgb([80, 180, 120]) },
warning: { default: rgb([220, 160, 80]) },
info: { base: rgb([40, 120, 220]) },
success: { base: rgb([80, 180, 120]) },
warning: { base: rgb([220, 160, 80]) },
},
},
background: { default: rgb([250, 250, 250]) },
background: { base: rgb([250, 250, 250]) },
categorical: [accent],
}
const palette = resolveOpenCodeDiagramPalette(theme, mode)
+12 -12
View File
@@ -46,27 +46,27 @@ export function createOpenCodeDiagramPalette(input: OpenCodeDiagramPaletteInput)
export function resolveOpenCodeDiagramPalette(
theme: {
readonly text: {
readonly default: RGBA
readonly subdued: RGBA
readonly base: RGBA
readonly muted: RGBA
readonly feedback: {
readonly info: { readonly default: RGBA }
readonly success: { readonly default: RGBA }
readonly warning: { readonly default: RGBA }
readonly info: { readonly base: RGBA }
readonly success: { readonly base: RGBA }
readonly warning: { readonly base: RGBA }
}
}
readonly background: { readonly default: RGBA }
readonly background: { readonly base: RGBA }
readonly categorical: readonly Readonly<Record<200 | 300 | 700 | 800, RGBA>>[]
},
mode: "dark" | "light",
) {
const accent = theme.categorical[3] ?? theme.categorical[0]!
return createOpenCodeDiagramPalette({
text: theme.text.default,
subdued: theme.text.subdued,
info: theme.text.feedback.info.default,
success: theme.text.feedback.success.default,
warning: theme.text.feedback.warning.default,
background: theme.background.default,
text: theme.text.base,
subdued: theme.text.muted,
info: theme.text.feedback.info.base,
success: theme.text.feedback.success.base,
warning: theme.text.feedback.warning.base,
background: theme.background.base,
accent: {
soft: accent[mode === "dark" ? 300 : 700],
clear: accent[mode === "dark" ? 200 : 800],
+17 -11
View File
@@ -2,21 +2,27 @@
import { $ } from "bun"
import { rm } from "node:fs/promises"
import path from "node:path"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
await rm("dist", { recursive: true, force: true })
await $`bun tsc -p tsconfig.build.json`
const files = await Array.fromAsync(new Bun.Glob("**/*.ts").scan({ cwd: "src" })).then((items) =>
items.map((item) => `src/${item}`),
)
const transpiler = new Bun.Transpiler({ loader: "ts", target: "bun" })
await Promise.all(
files.map(async (file) =>
Bun.write(
file.replace(/^src\//, "dist/").replace(/\.ts$/, ".js"),
await transpiler.transform(await Bun.file(file).text()),
),
const root = path.resolve("src")
const result = await Bun.build({
entrypoints: await Array.fromAsync(new Bun.Glob("**/*.ts").scan({ cwd: root, absolute: true })).then((files) =>
files.filter((file) => !file.endsWith(".d.ts")),
),
)
root,
outdir: "dist",
target: "node",
format: "esm",
packages: "external",
splitting: true,
naming: {
entry: "[dir]/[name].[ext]",
chunk: "chunks/[name]-[hash].[ext]",
},
})
if (!result.success) throw new AggregateError(result.logs, "Failed to build SDK")
+1
View File
@@ -213,6 +213,7 @@ for (const module of modules) {
throw new Error(`Packed SDK consumer resolved multiple Effect runtimes:\n${runtimes.join("\n")}`)
}
await $`bun imports.mjs`.cwd(consumer)
await $`node imports.mjs`.cwd(consumer)
await $`bun --conditions=workerd imports.mjs`.cwd(consumer)
await $`node_modules/.bin/wrangler deploy --dry-run --config wrangler.jsonc --outdir dist`.cwd(consumer)
+17 -11
View File
@@ -2,21 +2,27 @@
import { $ } from "bun"
import { rm } from "node:fs/promises"
import path from "node:path"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
await rm("dist", { recursive: true, force: true })
await $`bun tsc -p tsconfig.build.json`
const files = await Array.fromAsync(new Bun.Glob("**/*.ts").scan({ cwd: "src" })).then((items) =>
items.map((item) => `src/${item}`),
)
const transpiler = new Bun.Transpiler({ loader: "ts", target: "bun" })
await Promise.all(
files.map(async (file) =>
Bun.write(
file.replace(/^src\//, "dist/").replace(/\.ts$/, ".js"),
await transpiler.transform(await Bun.file(file).text()),
),
const root = path.resolve("src")
const result = await Bun.build({
entrypoints: await Array.fromAsync(new Bun.Glob("**/*.ts").scan({ cwd: root, absolute: true })).then((files) =>
files.filter((file) => !file.endsWith(".d.ts")),
),
)
root,
outdir: "dist",
target: "node",
format: "esm",
packages: "external",
splitting: true,
naming: {
entry: "[dir]/[name].[ext]",
chunk: "chunks/[name]-[hash].[ext]",
},
})
if (!result.success) throw new AggregateError(result.logs, "Failed to build Server")
+3
View File
@@ -0,0 +1,3 @@
import type { HueName } from "./schema.js"
export const DEFAULT_CATEGORICAL = ["blue", "purple", "green", "orange", "red", "cyan"] as const satisfies readonly HueName[]
-452
View File
@@ -1,452 +0,0 @@
import type { BaseThemeDefinition, HueName, Mode, ThemeDefinition, ThemeDocument } from "./schema.js"
export const DEFAULT_CATEGORICAL = [
"blue",
"purple",
"green",
"orange",
"red",
"cyan",
] as const satisfies readonly HueName[]
const modes = {
light: {
hue: {
gray: {
100: "#111827",
200: "#1f2937",
300: "#374151",
400: "#4b5563",
500: "#6b7280",
600: "#9ca3af",
700: "#d1d5db",
800: "#e5e7eb",
900: "#f3f4f6",
},
red: {
100: "#7f1d1d",
200: "#991b1b",
300: "#b91c1c",
400: "#dc2626",
500: "#ef4444",
600: "#f87171",
700: "#fca5a5",
800: "#fecaca",
900: "#fee2e2",
},
orange: {
100: "#7c2d12",
200: "#9a3412",
300: "#c2410c",
400: "#ea580c",
500: "#f97316",
600: "#fb923c",
700: "#fdba74",
800: "#fed7aa",
900: "#ffedd5",
},
yellow: {
100: "#713f12",
200: "#854d0e",
300: "#a16207",
400: "#ca8a04",
500: "#eab308",
600: "#facc15",
700: "#fde047",
800: "#fef08a",
900: "#fef9c3",
},
green: {
100: "#14532d",
200: "#166534",
300: "#15803d",
400: "#16a34a",
500: "#22c55e",
600: "#4ade80",
700: "#86efac",
800: "#bbf7d0",
900: "#dcfce7",
},
cyan: {
100: "#164e63",
200: "#155e75",
300: "#0e7490",
400: "#0891b2",
500: "#06b6d4",
600: "#22d3ee",
700: "#67e8f9",
800: "#a5f3fc",
900: "#cffafe",
},
blue: {
100: "#1e3a8a",
200: "#1e40af",
300: "#1d4ed8",
400: "#2563eb",
500: "#3b82f6",
600: "#60a5fa",
700: "#93c5fd",
800: "#bfdbfe",
900: "#dbeafe",
},
purple: {
100: "#581c87",
200: "#6b21a8",
300: "#7e22ce",
400: "#9333ea",
500: "#a855f7",
600: "#c084fc",
700: "#d8b4fe",
800: "#e9d5ff",
900: "#f3e8ff",
},
accent: "$hue.blue",
interactive: "$hue.blue",
neutral: "$hue.gray",
},
categorical: DEFAULT_CATEGORICAL,
text: {
default: "$hue.neutral.200",
subdued: "$hue.neutral.400",
action: {
primary: { default: "$hue.neutral.800", $disabled: "$hue.neutral.500" },
secondary: { default: "$text.subdued", $hovered: "$text.default" },
destructive: { default: "$hue.red.800", $disabled: "$hue.neutral.500" },
},
formfield: {
default: "$hue.neutral.200",
$focused: "$text.action.primary.default",
$pressed: "$hue.neutral.800",
$disabled: "$hue.neutral.500",
$selected: "$hue.interactive.300",
},
status: {
running: "$hue.interactive.200",
question: "$text.status.unread",
permission: "$text.status.unread",
unread: "$hue.accent.200",
},
feedback: {
error: { default: "$hue.red.300", subdued: "$hue.red.400" },
warning: { default: "$hue.yellow.200", subdued: "$hue.yellow.300" },
success: { default: "$hue.green.300", subdued: "$hue.green.400" },
info: { default: "$hue.cyan.300", subdued: "$hue.cyan.400" },
},
},
background: {
default: "$hue.neutral.800",
raised: {
base: "$hue.neutral.700",
high: "$hue.neutral.600",
max: "$hue.neutral.500",
},
action: {
primary: {
default: "$hue.interactive.400",
$hovered: "$hue.interactive.300",
$focused: "$hue.interactive.300",
$pressed: "$hue.interactive.200",
$selected: "$hue.interactive.300",
$disabled: "$hue.neutral.700",
},
secondary: { default: "transparent" },
destructive: {
default: "$hue.red.400",
$hovered: "$hue.red.300",
$focused: "$hue.red.300",
$pressed: "$hue.red.200",
$selected: "$hue.red.300",
$disabled: "$hue.neutral.700",
},
},
formfield: {
default: "$background.default",
$hovered: "$background.raised.base",
$focused: "$background.action.primary.default",
$pressed: "$hue.interactive.200",
$disabled: "$background.default",
$selected: "$background.formfield.default",
},
feedback: {
error: { default: "$background.default" },
warning: { default: "$background.default" },
success: { default: "$background.default" },
info: { default: "$background.default" },
},
},
border: { default: "$hue.neutral.700" },
scrollbar: { default: "$hue.neutral.600" },
diff: {
text: {
added: "$hue.green.300",
removed: "$hue.red.300",
context: "$hue.neutral.100",
hunkHeader: "$hue.purple.400",
},
background: { added: "$hue.green.900", removed: "$hue.red.900", context: "$hue.neutral.900" },
highlight: { added: "$hue.green.400", removed: "$hue.red.400" },
lineNumber: {
text: "$hue.neutral.400",
background: { added: "$hue.green.800", removed: "$hue.red.800" },
},
},
syntax: {
comment: "$hue.neutral.400",
keyword: "$hue.purple.400",
function: "$hue.accent.400",
variable: "$hue.neutral.100",
string: "$hue.green.300",
number: "$hue.yellow.200",
type: "$hue.yellow.500",
operator: "$hue.cyan.400",
punctuation: "$hue.neutral.100",
},
markdown: {
text: "$hue.neutral.100",
heading: "$hue.purple.400",
link: "$hue.accent.400",
linkText: "$hue.cyan.400",
code: "$hue.green.300",
blockQuote: "$hue.neutral.400",
emphasis: "$hue.yellow.500",
strong: "$hue.neutral.100",
horizontalRule: "$hue.neutral.700",
listItem: "$hue.accent.400",
listEnumeration: "$hue.cyan.400",
image: "$hue.accent.400",
imageText: "$hue.cyan.400",
codeBlock: "$hue.neutral.100",
},
"@dialog": {
text: { action: { primary: { default: "$hue.neutral.900" } } },
background: {
default: "$background.raised.base",
action: { primary: { default: "$hue.interactive.500", $hovered: "$background.raised.high" } },
},
},
},
dark: {
hue: {
gray: {
100: "#f3f4f6",
200: "#e5e7eb",
300: "#d1d5db",
400: "#9ca3af",
500: "#6b7280",
600: "#4b5563",
700: "#374151",
800: "#1f2937",
900: "#111827",
},
red: {
100: "#fee2e2",
200: "#fecaca",
300: "#fca5a5",
400: "#f87171",
500: "#ef4444",
600: "#dc2626",
700: "#b91c1c",
800: "#991b1b",
900: "#7f1d1d",
},
orange: {
100: "#ffedd5",
200: "#fed7aa",
300: "#fdba74",
400: "#fb923c",
500: "#f97316",
600: "#ea580c",
700: "#c2410c",
800: "#9a3412",
900: "#7c2d12",
},
yellow: {
100: "#fef9c3",
200: "#fef08a",
300: "#fde047",
400: "#facc15",
500: "#eab308",
600: "#ca8a04",
700: "#a16207",
800: "#854d0e",
900: "#713f12",
},
green: {
100: "#dcfce7",
200: "#bbf7d0",
300: "#86efac",
400: "#4ade80",
500: "#22c55e",
600: "#16a34a",
700: "#15803d",
800: "#166534",
900: "#14532d",
},
cyan: {
100: "#cffafe",
200: "#a5f3fc",
300: "#67e8f9",
400: "#22d3ee",
500: "#06b6d4",
600: "#0891b2",
700: "#0e7490",
800: "#155e75",
900: "#164e63",
},
blue: {
100: "#dbeafe",
200: "#bfdbfe",
300: "#93c5fd",
400: "#60a5fa",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
800: "#1e40af",
900: "#1e3a8a",
},
purple: {
100: "#f3e8ff",
200: "#e9d5ff",
300: "#d8b4fe",
400: "#c084fc",
500: "#a855f7",
600: "#9333ea",
700: "#7e22ce",
800: "#6b21a8",
900: "#581c87",
},
accent: "$hue.blue",
interactive: "$hue.blue",
neutral: "$hue.gray",
},
categorical: DEFAULT_CATEGORICAL,
text: {
default: "$hue.neutral.200",
subdued: "$hue.neutral.400",
action: {
primary: { default: "$hue.neutral.200", $disabled: "$hue.neutral.500" },
secondary: { default: "$text.subdued", $hovered: "$text.default" },
destructive: { default: "$hue.red.200", $disabled: "$hue.neutral.500" },
},
formfield: {
default: "$hue.neutral.200",
$focused: "$text.action.primary.default",
$pressed: "$hue.neutral.200",
$disabled: "$hue.neutral.500",
$selected: "$hue.interactive.500",
},
status: {
running: "$hue.interactive.200",
question: "$text.status.unread",
permission: "$text.status.unread",
unread: "$hue.accent.200",
},
feedback: {
error: { default: "$hue.red.300", subdued: "$hue.red.400" },
warning: { default: "$hue.yellow.200", subdued: "$hue.yellow.300" },
success: { default: "$hue.green.300", subdued: "$hue.green.400" },
info: { default: "$hue.cyan.300", subdued: "$hue.cyan.400" },
},
},
background: {
default: "$hue.neutral.800",
raised: {
base: "$hue.neutral.700",
high: "$hue.neutral.600",
max: "$hue.neutral.500",
},
action: {
primary: {
default: "$hue.interactive.500",
$hovered: "$hue.interactive.600",
$focused: "$hue.interactive.600",
$pressed: "$hue.interactive.800",
$selected: "$hue.interactive.600",
$disabled: "$hue.neutral.800",
},
secondary: { default: "transparent" },
destructive: {
default: "$hue.red.600",
$hovered: "$hue.red.700",
$focused: "$hue.red.700",
$pressed: "$hue.red.800",
$selected: "$hue.red.700",
$disabled: "$hue.neutral.800",
},
},
formfield: {
default: "$background.default",
$hovered: "$background.raised.base",
$focused: "$background.action.primary.default",
$pressed: "$hue.interactive.800",
$disabled: "$background.default",
$selected: "$background.formfield.default",
},
feedback: {
error: { default: "$background.default" },
warning: { default: "$background.default" },
success: { default: "$background.default" },
info: { default: "$background.default" },
},
},
border: { default: "$hue.neutral.700" },
scrollbar: { default: "$hue.neutral.600" },
diff: {
text: {
added: "$hue.green.300",
removed: "$hue.red.300",
context: "$hue.neutral.100",
hunkHeader: "$hue.purple.400",
},
background: { added: "$hue.green.900", removed: "$hue.red.900", context: "$hue.neutral.900" },
highlight: { added: "$hue.green.400", removed: "$hue.red.400" },
lineNumber: {
text: "$hue.neutral.400",
background: { added: "$hue.green.800", removed: "$hue.red.800" },
},
},
syntax: {
comment: "$hue.neutral.400",
keyword: "$hue.purple.400",
function: "$hue.accent.400",
variable: "$hue.neutral.100",
string: "$hue.green.300",
number: "$hue.yellow.200",
type: "$hue.yellow.500",
operator: "$hue.cyan.400",
punctuation: "$hue.neutral.100",
},
markdown: {
text: "$hue.neutral.100",
heading: "$hue.purple.400",
link: "$hue.accent.400",
linkText: "$hue.cyan.400",
code: "$hue.green.300",
blockQuote: "$hue.neutral.400",
emphasis: "$hue.yellow.500",
strong: "$hue.neutral.100",
horizontalRule: "$hue.neutral.700",
listItem: "$hue.accent.400",
listEnumeration: "$hue.cyan.400",
image: "$hue.accent.400",
imageText: "$hue.cyan.400",
codeBlock: "$hue.neutral.100",
},
"@dialog": {
text: { action: { primary: { default: "$hue.neutral.200" } } },
background: {
default: "$background.raised.base",
action: { primary: { default: "$hue.interactive.400", $hovered: "$background.raised.high" } },
},
},
},
} satisfies Record<Mode, ThemeDefinition>
const { hue: _, ...base } = modes.light
export const DEFAULT_THEME = {
version: 2,
base: base satisfies BaseThemeDefinition,
light: { hue: modes.light.hue },
dark: modes.dark,
} satisfies ThemeDocument
+6 -6
View File
@@ -40,7 +40,7 @@ function expandText(definition: TextDefinition | undefined): TextDefinition | un
if (!definition) return
return {
...definition,
subdued: definition.subdued ?? (definition.default ? "$text.default" : undefined),
muted: definition.muted ?? (definition.base ? "$text.base" : undefined),
action: expandActions(definition.action, "text.action"),
formfield: expandFormfield(definition.formfield, "text.formfield"),
feedback: definition.feedback
@@ -50,7 +50,7 @@ function expandText(definition: TextDefinition | undefined): TextDefinition | un
kind,
{
...feedback,
subdued: feedback.subdued ?? (feedback.default ? `$text.feedback.${kind}.default` : undefined),
muted: feedback.muted ?? (feedback.base ? `$text.feedback.${kind}.base` : undefined),
},
]
}),
@@ -69,11 +69,11 @@ function expandBackground(definition: BackgroundDefinition | undefined): Backgro
}
function expandFormfield(definition: StatefulColorDefinition | undefined, path: string) {
if (!definition?.default) return definition
if (!definition?.base) return definition
return {
...definition,
...Object.fromEntries(
ActionState.literals.map((state) => [`$${state}`, definition[`$${state}`] ?? `$${path}.default`]),
ActionState.literals.map((state) => [`$${state}`, definition[`$${state}`] ?? `$${path}.base`]),
),
}
}
@@ -85,13 +85,13 @@ function expandActions<Definition extends Partial<Record<string, StatefulColorDe
if (!definition) return
return Object.fromEntries(
Object.entries(definition).map(([variant, value]) => {
if (!value?.default) return [variant, value]
if (!value?.base) return [variant, value]
return [
variant,
{
...value,
...Object.fromEntries(
ActionState.literals.map((state) => [`$${state}`, value[`$${state}`] ?? `$${path}.${variant}.default`]),
ActionState.literals.map((state) => [`$${state}`, value[`$${state}`] ?? `$${path}.${variant}.base`]),
),
},
]
+1 -1
View File
@@ -42,7 +42,7 @@ export type {
ResolvedThemeTokens,
StatefulColor,
} from "./types.js"
export { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "./defaults.js"
export { DEFAULT_CATEGORICAL } from "./categorical.js"
export { expandTheme } from "./expand.js"
export { migrateV1 } from "./v1-migrate.js"
export { resolveTheme, resolveThemeDocument, themeDecodeError } from "./resolve.js"
+6 -6
View File
@@ -100,14 +100,14 @@ function contextualActions(
return [
variant,
Object.fromEntries(
(["default", ...ActionState.literals] as readonly ResolvedActionState[]).map((state) => {
const key = state === "default" ? undefined : (`$${state}` as ActionStateKey)
(["base", ...ActionState.literals] as readonly ResolvedActionState[]).map((state) => {
const key = state === "base" ? undefined : (`$${state}` as ActionStateKey)
return [
key ?? "default",
key ?? "base",
(key ? surfaceVariant?.[key] : undefined) ??
surfaceVariant?.default ??
surfaceVariant?.base ??
(key ? baseVariant?.[key] : undefined) ??
baseVariant?.default,
baseVariant?.base,
]
}),
),
@@ -151,7 +151,7 @@ function statefulActions(actions: Readonly<Record<ActionVariant, StatefulColor>>
function statefulColor(color: StatefulColor): StatefulColor {
return {
...color,
state: (states: ActionStates) => color[ActionState.literals.find((state) => states[state]) ?? "default"],
state: (states: ActionStates) => color[ActionState.literals.find((state) => states[state]) ?? "base"],
}
}
+17 -32
View File
@@ -62,7 +62,7 @@ const HueDefinition = Schema.Struct({
export type HueDefinition = Schema.Schema.Type<typeof HueDefinition>
const StatefulColorDefinition = Schema.Struct({
default: Schema.optional(ColorValue),
base: Schema.optional(ColorValue),
$hovered: Schema.optional(ColorValue),
$focused: Schema.optional(ColorValue),
$pressed: Schema.optional(ColorValue),
@@ -80,27 +80,19 @@ const ActionColorDefinition = Schema.Struct({
})
const TextFeedbackDefinition = Schema.Struct({
default: Schema.optional(ColorValue),
subdued: Schema.optional(ColorValue),
base: Schema.optional(ColorValue),
muted: Schema.optional(ColorValue),
})
const BackgroundFeedbackDefinition = Schema.Struct({
default: Schema.optional(ColorValue),
base: Schema.optional(ColorValue),
})
const TextDefinition = Schema.Struct({
default: Schema.optional(ColorValue),
subdued: Schema.optional(ColorValue),
base: Schema.optional(ColorValue),
muted: Schema.optional(ColorValue),
action: Schema.optional(ActionColorDefinition),
formfield: Schema.optional(StatefulColorDefinition),
status: Schema.optional(
Schema.Struct({
running: Schema.optional(ColorValue),
question: Schema.optional(ColorValue),
permission: Schema.optional(ColorValue),
unread: Schema.optional(ColorValue),
}),
),
feedback: Schema.optional(
Schema.Struct({
error: Schema.optional(TextFeedbackDefinition),
@@ -113,7 +105,7 @@ const TextDefinition = Schema.Struct({
export type TextDefinition = Schema.Schema.Type<typeof TextDefinition>
const BackgroundDefinition = Schema.Struct({
default: Schema.optional(ColorValue),
base: Schema.optional(ColorValue),
raised: Schema.optional(
Schema.Struct({
base: Schema.optional(ColorValue),
@@ -202,8 +194,8 @@ export type DiffDefinition = Schema.Schema.Type<typeof DiffDefinition>
const ThemeTokensDefinition = Schema.Struct({
text: Schema.optional(TextDefinition),
background: Schema.optional(BackgroundDefinition),
border: Schema.optional(Schema.Struct({ default: Schema.optional(ColorValue) })),
scrollbar: Schema.optional(Schema.Struct({ default: Schema.optional(ColorValue) })),
border: Schema.optional(Schema.Struct({ base: Schema.optional(ColorValue) })),
scrollbar: Schema.optional(Schema.Struct({ base: Schema.optional(ColorValue) })),
diff: Schema.optional(DiffDefinition),
syntax: Schema.optional(SyntaxDefinition),
markdown: Schema.optional(MarkdownDefinition),
@@ -211,7 +203,7 @@ const ThemeTokensDefinition = Schema.Struct({
export type ThemeTokensDefinition = Schema.Schema.Type<typeof ThemeTokensDefinition>
const CompleteStatefulColorDefinition = Schema.Struct({
default: ColorValue,
base: ColorValue,
$hovered: Schema.optional(ColorValue),
$focused: Schema.optional(ColorValue),
$pressed: Schema.optional(ColorValue),
@@ -225,21 +217,15 @@ const CompleteActionColorDefinition = Schema.Struct({
destructive: CompleteStatefulColorDefinition,
})
const CompleteTextFeedbackDefinition = Schema.Struct({ default: ColorValue, subdued: Schema.optional(ColorValue) })
const CompleteBackgroundFeedbackDefinition = Schema.Struct({ default: ColorValue })
const CompleteTextFeedbackDefinition = Schema.Struct({ base: ColorValue, muted: Schema.optional(ColorValue) })
const CompleteBackgroundFeedbackDefinition = Schema.Struct({ base: ColorValue })
const CompleteThemeTokensDefinition = Schema.Struct({
text: Schema.Struct({
default: ColorValue,
subdued: ColorValue,
base: ColorValue,
muted: ColorValue,
action: CompleteActionColorDefinition,
formfield: CompleteStatefulColorDefinition,
status: Schema.Struct({
running: ColorValue,
question: ColorValue,
permission: ColorValue,
unread: ColorValue,
}),
feedback: Schema.Struct({
error: CompleteTextFeedbackDefinition,
warning: CompleteTextFeedbackDefinition,
@@ -248,7 +234,7 @@ const CompleteThemeTokensDefinition = Schema.Struct({
}),
}),
background: Schema.Struct({
default: ColorValue,
base: ColorValue,
raised: Schema.Struct({ base: ColorValue, high: ColorValue, max: ColorValue }),
action: CompleteActionColorDefinition,
formfield: CompleteStatefulColorDefinition,
@@ -259,8 +245,8 @@ const CompleteThemeTokensDefinition = Schema.Struct({
info: CompleteBackgroundFeedbackDefinition,
}),
}),
border: Schema.Struct({ default: ColorValue }),
scrollbar: Schema.Struct({ default: ColorValue }),
border: Schema.Struct({ base: ColorValue }),
scrollbar: Schema.Struct({ base: ColorValue }),
diff: Schema.Struct({
text: Schema.Struct({ added: ColorValue, removed: ColorValue, context: ColorValue, hunkHeader: ColorValue }),
background: Schema.Struct({ added: ColorValue, removed: ColorValue, context: ColorValue }),
@@ -300,7 +286,6 @@ export type ModeDefinition = Schema.Schema.Type<typeof ModeDefinition>
const FileMetadata = {
$schema: Schema.optional(Schema.String),
version: Schema.Literal(2),
}
export const ThemeDocument = Schema.Union([
Schema.Struct({
+17 -17
View File
@@ -8,14 +8,14 @@ export function generateSyntax(theme: ResolvedThemeTokens) {
const feedback = theme.text.feedback
return SyntaxStyle.fromTheme([
rule(["default"], theme.text.default),
rule(["default"], theme.text.base),
rule(["prompt"], theme.hue.accent[step]),
rule(["extmark.file"], feedback.warning.default, { bold: true }),
rule(["extmark.file"], feedback.warning.base, { bold: true }),
rule(["extmark.agent"], theme.categorical[0][step], { bold: true }),
rule(["extmark.skill"], (theme.categorical[1] ?? theme.categorical[0])[step], { bold: true }),
// V1 migration preserves its selected/inverse foreground in this action state.
rule(["extmark.paste"], theme.text.action.primary.focused, {
background: feedback.warning.default,
background: feedback.warning.base,
bold: true,
}),
rule(["comment", "comment.documentation"], syntax.comment, { italic: true }),
@@ -39,7 +39,7 @@ export function generateSyntax(theme: ResolvedThemeTokens) {
rule(["punctuation", "punctuation.bracket"], syntax.punctuation),
rule(
["variable.builtin", "type.builtin", "function.builtin", "module.builtin", "constant.builtin", "variable.super"],
feedback.error.default,
feedback.error.base,
),
rule(["keyword.directive", "keyword.modifier", "keyword.exception"], syntax.keyword, { italic: true }),
rule(["punctuation.special", "tag.delimiter"], syntax.operator),
@@ -61,27 +61,27 @@ export function generateSyntax(theme: ResolvedThemeTokens) {
rule(["markup.list"], markdown.listItem),
rule(["markup.quote"], markdown.blockQuote, { italic: true }),
rule(["markup.raw", "markup.raw.block"], markdown.code),
rule(["markup.raw.inline"], markdown.code, { background: theme.background.default }),
rule(["markup.raw.inline"], markdown.code, { background: theme.background.base }),
rule(["markup.link", "markup.link.url", "string.special", "string.special.url"], markdown.link, {
underline: true,
}),
rule(["markup.link.label"], markdown.linkText, { underline: true }),
rule(["label"], markdown.linkText),
rule(["spell", "nospell"], theme.text.default),
rule(["markup.underline"], theme.text.default, { underline: true }),
rule(["comment.error"], feedback.error.default, { italic: true, bold: true }),
rule(["comment.warning"], feedback.warning.default, { italic: true, bold: true }),
rule(["comment.todo", "comment.note"], feedback.info.default, { italic: true, bold: true }),
rule(["attribute", "annotation"], feedback.warning.default),
rule(["tag"], feedback.error.default),
rule(["markup.strikethrough", "markup.list.unchecked", "debug"], theme.text.subdued),
rule(["markup.list.checked"], feedback.success.default),
rule(["spell", "nospell"], theme.text.base),
rule(["markup.underline"], theme.text.base, { underline: true }),
rule(["comment.error"], feedback.error.base, { italic: true, bold: true }),
rule(["comment.warning"], feedback.warning.base, { italic: true, bold: true }),
rule(["comment.todo", "comment.note"], feedback.info.base, { italic: true, bold: true }),
rule(["attribute", "annotation"], feedback.warning.base),
rule(["tag"], feedback.error.base),
rule(["markup.strikethrough", "markup.list.unchecked", "debug"], theme.text.muted),
rule(["markup.list.checked"], feedback.success.base),
rule(["diff.plus"], theme.diff.text.added, { background: theme.diff.background.added }),
rule(["diff.minus"], theme.diff.text.removed, { background: theme.diff.background.removed }),
rule(["diff.delta"], theme.diff.text.context, { background: theme.diff.background.context }),
rule(["error"], feedback.error.default, { bold: true }),
rule(["warning"], feedback.warning.default, { bold: true }),
rule(["info"], feedback.info.default),
rule(["error"], feedback.error.base, { bold: true }),
rule(["warning"], feedback.warning.base, { bold: true }),
rule(["info"], feedback.info.base),
])
}
+8 -14
View File
@@ -11,7 +11,7 @@ import type {
SyntaxToken,
} from "./schema.js"
export type ResolvedActionState = "default" | ActionState
export type ResolvedActionState = "base" | ActionState
export type ResolvedFormfieldState = ResolvedActionState
export type HueScale = Readonly<Record<HueStep, RGBA>>
export type Hue = Readonly<Record<BaseHue | HueAlias, HueScale>>
@@ -30,20 +30,14 @@ export type ResolvedThemeTokens = {
readonly increase: (color: RGBA, amount?: number) => RGBA
readonly decrease: (color: RGBA, amount?: number) => RGBA
readonly text: {
readonly default: RGBA
readonly subdued: RGBA
readonly base: RGBA
readonly muted: RGBA
readonly action: Readonly<Record<ActionVariant, StatefulColor>>
readonly formfield: FormfieldColor
readonly status: {
readonly running: RGBA
readonly question: RGBA
readonly permission: RGBA
readonly unread: RGBA
}
readonly feedback: Readonly<Record<FeedbackKind, { readonly default: RGBA; readonly subdued: RGBA }>>
readonly feedback: Readonly<Record<FeedbackKind, { readonly base: RGBA; readonly muted: RGBA }>>
}
readonly background: {
readonly default: RGBA
readonly base: RGBA
readonly raised: {
readonly base: RGBA
readonly high: RGBA
@@ -51,10 +45,10 @@ export type ResolvedThemeTokens = {
}
readonly action: Readonly<Record<ActionVariant, StatefulColor>>
readonly formfield: FormfieldColor
readonly feedback: Readonly<Record<FeedbackKind, { readonly default: RGBA }>>
readonly feedback: Readonly<Record<FeedbackKind, { readonly base: RGBA }>>
}
readonly border: { readonly default: RGBA }
readonly scrollbar: { readonly default: RGBA }
readonly border: { readonly base: RGBA }
readonly scrollbar: { readonly base: RGBA }
readonly diff: {
readonly text: {
readonly added: RGBA
+25 -32
View File
@@ -1,6 +1,6 @@
import { RGBA } from "@opentui/core"
import { oklchToHex, rgbToOklch } from "./color.js"
import { DEFAULT_CATEGORICAL } from "./defaults.js"
import { DEFAULT_CATEGORICAL } from "./categorical.js"
import type { BaseThemeDefinition, HueDefinition, Mode, ThemeDefinition, ThemeDocument } from "./index.js"
import { HueStep } from "./schema.js"
import type { Theme, ThemeV1Json } from "./v1.js"
@@ -50,14 +50,13 @@ export function migrateV1(theme: ThemeV1Json): ThemeDocument {
const darkMode = detectMode(dark)
if (lightMode === darkMode) {
const definition = migrateMode(lightMode === "light" ? light : dark, lightMode)
if (lightMode === "light") return { version: 2, base: base(definition), light: { hue: definition.hue } }
return { version: 2, base: base(definition), dark: { hue: definition.hue } }
if (lightMode === "light") return { base: base(definition), light: { hue: definition.hue } }
return { base: base(definition), dark: { hue: definition.hue } }
}
}
const lightDefinition = migrateMode(light, "light")
const darkDefinition = migrateMode(dark, "dark")
return {
version: 2,
base: base(lightDefinition),
light: { hue: lightDefinition.hue },
dark: darkDefinition,
@@ -110,63 +109,57 @@ function migrateMode(theme: Theme, mode: Mode): ThemeDefinition {
} as HueDefinition,
categorical: uniqueCategorical.length ? uniqueCategorical : DEFAULT_CATEGORICAL,
text: {
default: text,
subdued: textMuted,
base: text,
muted: textMuted,
action: {
primary: {
default: "$text.default",
base: "$text.base",
$disabled: textMuted,
$focused: selected,
$selected: primary,
},
secondary: { default: "$text.subdued", $hovered: "$text.default" },
destructive: { default: destructive, $disabled: textMuted },
secondary: { base: "$text.muted", $hovered: "$text.base" },
destructive: { base: destructive, $disabled: textMuted },
},
formfield: {
default: text,
base: text,
$hovered: primary,
$focused: primary,
$pressed: primary,
$disabled: textMuted,
$selected: primary,
},
status: {
running: "$hue.interactive.200",
question: "$text.status.unread",
permission: "$text.status.unread",
unread: "$hue.accent.200",
},
feedback: {
error: { default: color("error") },
warning: { default: color("warning") },
success: { default: color("success") },
info: { default: color("info") },
error: { base: color("error") },
warning: { base: color("warning") },
success: { base: color("success") },
info: { base: color("info") },
},
},
background: {
default: background,
base: background,
raised: {
base: backgroundPanel,
high: backgroundMenu,
max: backgroundRaisedMax,
},
action: {
primary: { default: "transparent", $hovered: backgroundPanel, $focused: primary, $selected: "transparent" },
secondary: { default: "transparent" },
destructive: { default: color("error") },
primary: { base: "transparent", $hovered: backgroundPanel, $focused: primary, $selected: "transparent" },
secondary: { base: "transparent" },
destructive: { base: color("error") },
},
formfield: {
default: "$background.default",
base: "$background.base",
},
feedback: {
error: { default: "$background.default" },
warning: { default: "$background.default" },
success: { default: "$background.default" },
info: { default: "$background.default" },
error: { base: "$background.base" },
warning: { base: "$background.base" },
success: { base: "$background.base" },
info: { base: "$background.base" },
},
},
border: { default: color("border") },
scrollbar: { default: color("borderActive") },
border: { base: color("border") },
scrollbar: { base: color("borderActive") },
diff: {
text: {
added: color("diffAdded"),
@@ -217,7 +210,7 @@ function migrateMode(theme: Theme, mode: Mode): ThemeDefinition {
},
"@dialog": {
background: {
default: "$background.raised.base",
base: "$background.raised.base",
action: { primary: { $hovered: "$background.raised.high" } },
},
},
-73
View File
@@ -1,73 +0,0 @@
import { expect, test } from "bun:test"
import { RGBA } from "@opentui/core"
import { Schema } from "effect"
import { DEFAULT_THEME, ThemeDocument, migrateV1, resolveThemeDocument, selectTheme } from "../src/tui/index.js"
import type { ThemeV1Json } from "../src/tui/v1.js"
test.each(["light", "dark"] as const)("built-in %s themes resolve status colors", async (mode) => {
const source: ThemeV1Json = await Bun.file(
new URL("../../tui/src/theme/assets/opencode.json", import.meta.url),
).json()
for (const document of [DEFAULT_THEME, migrateV1(source)]) {
const theme = resolveThemeDocument(document, mode)
expect(theme.text.status.running.equals(theme.hue.interactive[200])).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.text.status.unread.equals(theme.hue.accent[200])).toBeTrue()
expect(theme.surface("dialog").text.status).toEqual(theme.text.status)
}
})
test.each(["light", "dark"] as const)("custom %s themes inherit the unread attention color", (mode) => {
const base = selectTheme(DEFAULT_THEME, mode)
const definition = {
...base,
hue: { ...base.hue, accent: "$hue.purple" },
text: {
...base.text,
status: { ...base.text.status, unread: "#abcdef" },
},
}
const { hue, ...tokens } = definition
const theme = resolveThemeDocument(
Schema.decodeUnknownSync(ThemeDocument)({
version: 2,
base: tokens,
[mode]: { hue },
}),
mode,
)
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.surface("dialog").text.status).toEqual(theme.text.status)
})
test.each(["light", "dark"] as const)("custom %s themes inherit and override status colors", (mode) => {
const base = selectTheme(DEFAULT_THEME, mode)
const definition = {
...base,
hue: { ...base.hue, interactive: "$hue.purple", accent: "$hue.orange" },
text: {
...base.text,
status: {
...base.text.status,
question: "#123456",
permission: "#654321",
},
},
}
const { hue, ...tokens } = definition
const theme = resolveThemeDocument(
Schema.decodeUnknownSync(ThemeDocument)({
version: 2,
base: tokens,
[mode]: { hue },
}),
mode,
)
expect(theme.text.status.running.equals(theme.hue.purple[200])).toBeTrue()
expect(theme.text.status.unread.equals(theme.hue.orange[200])).toBeTrue()
expect(theme.text.status.question.equals(RGBA.fromHex("#123456"))).toBeTrue()
expect(theme.text.status.permission.equals(RGBA.fromHex("#654321"))).toBeTrue()
})
+15
View File
@@ -0,0 +1,15 @@
import { expect, test } from "bun:test"
import { migrateV1, resolveThemeDocument } from "../src/tui/index.js"
import type { ThemeV1Json } from "../src/tui/v1.js"
const source: ThemeV1Json = await Bun.file(
new URL("../../tui/src/theme/assets/opencode.json", import.meta.url),
).json()
const document = migrateV1(source)
test.each(["light", "dark"] as const)("resolves %s themes without status tokens", (mode) => {
const theme = resolveThemeDocument(document, mode)
expect("status" in theme.text).toBeFalse()
expect(theme.hue.accent[800]).toBeDefined()
expect(theme.hue.interactive[800]).toBeDefined()
})
+1 -1
View File
@@ -1311,7 +1311,7 @@ function App(props: { pair?: DialogPairCredentials }) {
width={dimensions().width}
height={dimensions().height}
flexDirection="column"
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
onMouseDown={(evt) => {
if (copyOnSelectEnabled()) return
if (evt.button !== MouseButton.RIGHT) return
+26 -26
View File
@@ -229,7 +229,7 @@ export function DevToolsBar() {
}
return (
<box height={1} flexShrink={0} flexDirection="row" backgroundColor={theme.decrease(theme.background.default)}>
<box height={1} flexShrink={0} flexDirection="row" backgroundColor={theme.decrease(theme.background.base)}>
<Show when={panel()}>
<box
position="absolute"
@@ -248,10 +248,10 @@ export function DevToolsBar() {
panel() === "server"
? theme.text.action.primary.focused
: serverIndicator().state === "connected"
? theme.text.feedback.success.default
? theme.text.feedback.success.base
: serverIndicator().state === "disconnected"
? theme.text.feedback.error.default
: theme.text.default
? theme.text.feedback.error.base
: theme.text.base
}
>
{serverIndicator().icon}
@@ -261,8 +261,8 @@ export function DevToolsBar() {
panel() === "server"
? theme.text.action.primary.focused
: serverIndicator().state === "disconnected"
? theme.text.feedback.error.default
: theme.text.subdued
? theme.text.feedback.error.base
: theme.text.muted
}
>
{" "}
@@ -286,7 +286,7 @@ export function DevToolsBar() {
)}
</Show>
<Show when={server.error}>
<text fg={elevatedTheme.text.feedback.error.default}>Server details unavailable</text>
<text fg={elevatedTheme.text.feedback.error.base}>Server details unavailable</text>
</Show>
</PanelBox>
</Show>
@@ -297,8 +297,8 @@ export function DevToolsBar() {
panel() === "ui"
? theme.text.action.primary.focused
: runtime() === "high"
? theme.text.feedback.error.default
: theme.text.subdued
? theme.text.feedback.error.base
: theme.text.muted
}
>
{statusIcon(runtime())}
@@ -308,8 +308,8 @@ export function DevToolsBar() {
panel() === "ui"
? theme.text.action.primary.focused
: runtime() === "high"
? theme.text.feedback.error.default
: theme.text.subdued
? theme.text.feedback.error.base
: theme.text.muted
}
>
{" "}
@@ -339,7 +339,7 @@ export function DevToolsBar() {
</Show>
</BarItem>
<BarItem active={panel() === "theme"} onClick={() => toggle("theme")}>
<text fg={panel() === "theme" ? theme.text.action.primary.focused : theme.text.subdued}>Theme</text>
<text fg={panel() === "theme" ? theme.text.action.primary.focused : theme.text.muted}>Theme</text>
<Show when={panel() === "theme"}>
<PanelBox>
<PanelTitle>Theme</PanelTitle>
@@ -355,7 +355,7 @@ export function DevToolsBar() {
</Show>
</BarItem>
<BarItem active={panel() === "tools"} onClick={() => toggle("tools")}>
<text fg={panel() === "tools" ? theme.text.action.primary.focused : theme.text.subdued}>Tools</text>
<text fg={panel() === "tools" ? theme.text.action.primary.focused : theme.text.muted}>Tools</text>
<Show when={panel() === "tools"}>
<PanelBox>
<PanelTitle>Tools</PanelTitle>
@@ -364,20 +364,20 @@ export function DevToolsBar() {
</Action>
<Show when={dumpPath()}>
{(file) => (
<text fg={elevatedTheme.text.subdued} wrapMode="word">
<text fg={elevatedTheme.text.muted} wrapMode="word">
{file()}
</text>
)}
</Show>
<Show when={dumpError()}>
{(error) => (
<text fg={elevatedTheme.text.feedback.error.default} wrapMode="word">
<text fg={elevatedTheme.text.feedback.error.base} wrapMode="word">
{error()}
</text>
)}
</Show>
<box marginTop={1}>
<text fg={elevatedTheme.text.default} attributes={TextAttributes.BOLD}>
<text fg={elevatedTheme.text.base} attributes={TextAttributes.BOLD}>
Render
</text>
<Action
@@ -416,7 +416,7 @@ export function DevToolsBar() {
<For each={groups()}>
{(group) => (
<box marginTop={1}>
<text fg={elevatedTheme.text.default} attributes={TextAttributes.BOLD}>
<text fg={elevatedTheme.text.base} attributes={TextAttributes.BOLD}>
{group.title}
</text>
<For each={group.entries}>{(entry) => <Row label={entry.key} value={String(entry.value)} />}</For>
@@ -433,10 +433,10 @@ export function DevToolsBar() {
dialog.replace(() => <DialogExperiments />)
}}
>
<text fg={theme.text.subdued}>Experiments</text>
<text fg={theme.text.muted}>Experiments</text>
</BarItem>
<box flexGrow={1} minWidth={0}>
<TimeToFirstDraw visible={timing()} width="100%" fg={theme.text.subdued} label="Time to first draw" />
<TimeToFirstDraw visible={timing()} width="100%" fg={theme.text.muted} label="Time to first draw" />
</box>
</box>
)
@@ -502,7 +502,7 @@ function PanelBox(props: ParentProps) {
function PanelTitle(props: ParentProps) {
const theme = useTheme()
return (
<text fg={theme.text.default} attributes={TextAttributes.BOLD} marginBottom={1}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD} marginBottom={1}>
{props.children}
</text>
)
@@ -512,9 +512,9 @@ function Row(props: { label: string; value: string }) {
const theme = useTheme()
return (
<box flexDirection="row">
<text fg={theme.text.subdued}>{props.label}</text>
<text fg={theme.text.muted}>{props.label}</text>
<box flexGrow={1} />
<text fg={theme.text.default}>{props.value}</text>
<text fg={theme.text.base}>{props.value}</text>
</box>
)
}
@@ -534,7 +534,7 @@ function Action(props: ParentProps<{ onClick: () => void; disabled?: boolean; ho
if (!props.disabled) props.onClick()
}}
>
<text fg={props.disabled ? theme.text.subdued : theme.text.action.primary.default}>{props.children}</text>
<text fg={props.disabled ? theme.text.muted : theme.text.action.primary.base}>{props.children}</text>
</box>
)
}
@@ -554,13 +554,13 @@ function ProcessStat(props: { label: string; values: readonly number[]; unit: st
return (
<box flexDirection="row">
<box width={7}>
<text fg={theme.text.subdued}>{props.label}</text>
<text fg={theme.text.muted}>{props.label}</text>
</box>
<box flexGrow={1}>
<text fg={props.values.length ? theme.text.default : theme.text.subdued}>{brailleGraph(props.values)}</text>
<text fg={props.values.length ? theme.text.base : theme.text.muted}>{brailleGraph(props.values)}</text>
</box>
<box width={8} alignItems="flex-end">
<text fg={props.values.length ? theme.text.default : theme.text.subdued}>{value()}</text>
<text fg={props.values.length ? theme.text.base : theme.text.muted}>{value()}</text>
</box>
</box>
)
+7 -7
View File
@@ -55,10 +55,10 @@ export function DialogDebug() {
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
Debug
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -68,10 +68,10 @@ export function DialogDebug() {
<For each={entries()}>
{(entry) => (
<box flexDirection="row" gap={1}>
<text flexShrink={0} fg={theme.text.subdued}>
<text flexShrink={0} fg={theme.text.muted}>
{entry.label.padEnd(10)}
</text>
<text fg={theme.text.default} wrapMode="word">
<text fg={theme.text.base} wrapMode="word">
{entry.value}
</text>
</box>
@@ -79,12 +79,12 @@ export function DialogDebug() {
</For>
</box>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text.subdued}>Share this when reporting an issue.</text>
<text fg={theme.text.muted}>Share this when reporting an issue.</text>
<text onMouseUp={copy}>
<span style={{ fg: copied() ? theme.text.feedback.success.default : theme.text.default }}>
<span style={{ fg: copied() ? theme.text.feedback.success.base : theme.text.base }}>
<b>{copied() ? "✓ copied" : "copy"}</b>{" "}
</span>
<span style={{ fg: theme.text.subdued }}>enter</span>
<span style={{ fg: theme.text.muted }}>enter</span>
</text>
</box>
</box>
@@ -76,7 +76,7 @@ export function DialogErrorDetails(props: {
<box flexDirection="row" gap={2}>
<text
attributes={TextAttributes.BOLD}
fg={theme.text.default}
fg={theme.text.base}
flexGrow={1}
minWidth={0}
wrapMode="none"
@@ -84,7 +84,7 @@ export function DialogErrorDetails(props: {
>
{props.title}
</text>
<text fg={theme.text.subdued} flexShrink={0} onMouseUp={props.onBack}>
<text fg={theme.text.muted} flexShrink={0} onMouseUp={props.onBack}>
esc
</text>
</box>
@@ -93,7 +93,7 @@ export function DialogErrorDetails(props: {
<FilePath
value={source()}
maxWidth={Math.min(dialogWidth(dialog.size), dimensions().width - 2) - 4}
fg={theme.text.subdued}
fg={theme.text.muted}
/>
)}
</Show>
@@ -106,28 +106,28 @@ export function DialogErrorDetails(props: {
scrollbarOptions={{ visible: false }}
scrollAcceleration={getScrollAcceleration(config)}
>
<text fg={theme.text.default} wrapMode="word">
<text fg={theme.text.base} wrapMode="word">
{props.error}
</text>
</scrollbox>
<Show when={props.diagnosticRef}>
<text fg={theme.text.subdued}>Reference: {props.diagnosticRef}</text>
<text fg={theme.text.muted}>Reference: {props.diagnosticRef}</text>
</Show>
</box>
<box flexDirection="row" gap={3} flexWrap="wrap">
<text onMouseUp={investigate}>
<span style={{ fg: theme.text.default }}>
<span style={{ fg: theme.text.base }}>
<b>i</b>
</span>
<span style={{ fg: theme.text.subdued }}> investigate</span>
<span style={{ fg: theme.text.muted }}> investigate</span>
</text>
<text onMouseUp={copy}>
<span style={{ fg: copied() ? theme.text.feedback.success.default : theme.text.default }}>
<span style={{ fg: copied() ? theme.text.feedback.success.base : theme.text.base }}>
<b>{copied() ? "✓ copied" : "c"}</b>
</span>
<span style={{ fg: theme.text.subdued }}>{copied() ? "" : " copy details"}</span>
<span style={{ fg: theme.text.muted }}>{copied() ? "" : " copy details"}</span>
</text>
<text fg={theme.text.subdued}>/ scroll</text>
<text fg={theme.text.muted}>/ scroll</text>
</box>
</box>
)
@@ -57,7 +57,7 @@ export function DialogExperiments() {
onSelect={(option) => void change(option.value)}
emptyView={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No experiments available</text>
<text fg={theme.text.muted}>No experiments available</text>
</box>
}
footerHints={experiments.length > 0 ? [{ title: "←/→", label: "change" }] : []}
@@ -39,10 +39,10 @@ export function DialogImagePreview(props: { images: readonly ImagePreviewItem[];
return (
<box id="prompt-image-viewer" paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
Image {index() + 1} of {props.images.length}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -56,13 +56,13 @@ export function DialogImagePreview(props: { images: readonly ImagePreviewItem[];
onError={() => setFailed(true)}
/>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text.subdued} onMouseUp={() => move(-1)}>
<text fg={theme.text.muted} onMouseUp={() => move(-1)}>
{props.images.length > 1 ? "← previous" : ""}
</text>
<text fg={failed() ? theme.text.feedback.error.default : theme.text.subdued} wrapMode="none" truncate>
<text fg={failed() ? theme.text.feedback.error.base : theme.text.muted} wrapMode="none" truncate>
{failed() ? "No preview" : (current().mention?.text ?? `Image ${index() + 1}`)}
</text>
<text fg={theme.text.subdued} onMouseUp={() => move(1)}>
<text fg={theme.text.muted} onMouseUp={() => move(1)}>
{props.images.length > 1 ? "next →" : ""}
</text>
</box>
@@ -114,7 +114,7 @@ export function DialogIntegration(
disabled: methods.length === 0 && credentials.length === 0,
gutter:
integration.connections.length > 0
? () => <text fg={theme.text.feedback.success.default}></text>
? () => <text fg={theme.text.feedback.success.base}></text>
: undefined,
onSelect: () => {
if (credentials.length) return manageConnections(integration, methods, location, dialog, props.onConnected)
@@ -130,12 +130,12 @@ export function DialogIntegration(
options={options()}
emptyView={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No integrations available</text>
<text fg={theme.text.muted}>No integrations available</text>
</box>
}
noMatchView={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No integrations found</text>
<text fg={theme.text.muted}>No integrations found</text>
</box>
}
/>
@@ -433,10 +433,10 @@ function CommandView(props: { title: string; output: string; message: string })
return (
<box gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{props.title}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc close
</text>
</box>
@@ -447,10 +447,10 @@ function CommandView(props: { title: string; output: string; message: string })
paddingTop={1}
paddingBottom={1}
>
<text fg={overlayTheme.text.default}>{props.output.trim()}</text>
<text fg={overlayTheme.text.base}>{props.output.trim()}</text>
</box>
<box paddingLeft={2} paddingRight={2}>
<text fg={theme.text.subdued}>{props.message}</text>
<text fg={theme.text.muted}>{props.message}</text>
</box>
</box>
)
@@ -487,7 +487,7 @@ function KeyMethod(props: {
.catch((cause) => setError(message(cause)))
}}
description={() => (
<Show when={error()}>{(value) => <text fg={theme.text.feedback.error.default}>{value()}</text>}</Show>
<Show when={error()}>{(value) => <text fg={theme.text.feedback.error.base}>{value()}</text>}</Show>
)}
/>
)
@@ -706,9 +706,9 @@ function OAuthCode(props: {
}}
description={() => (
<box gap={1}>
<text fg={theme.text.subdued}>{props.attempt.instructions}</text>
<text fg={theme.text.muted}>{props.attempt.instructions}</text>
<Link href={props.attempt.url} fg={theme.markdown.link} />
<Show when={error()}>{(value) => <text fg={theme.text.feedback.error.default}>{value()}</text>}</Show>
<Show when={error()}>{(value) => <text fg={theme.text.feedback.error.base}>{value()}</text>}</Show>
</box>
)}
/>
@@ -728,10 +728,10 @@ function OAuthView(props: {
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{props.title}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -740,21 +740,21 @@ function OAuthView(props: {
<box gap={1}>
<Link href={url()} fg={theme.markdown.link} />
<Show when={props.instructions}>
{(instructions) => <text fg={theme.text.subdued}>{instructions()}</text>}
{(instructions) => <text fg={theme.text.muted}>{instructions()}</text>}
</Show>
</box>
)}
</Show>
<text fg={theme.text.subdued}>{props.message}</text>
<text fg={theme.text.muted}>{props.message}</text>
<box flexDirection="row" gap={2}>
<Show when={props.open}>
<text fg={theme.text.default}>
o <span style={{ fg: theme.text.subdued }}>open</span>
<text fg={theme.text.base}>
o <span style={{ fg: theme.text.muted }}>open</span>
</text>
</Show>
<Show when={props.copy}>
<text fg={theme.text.default}>
c <span style={{ fg: theme.text.subdued }}>copy</span>
<text fg={theme.text.base}>
c <span style={{ fg: theme.text.muted }}>copy</span>
</text>
</Show>
</box>
@@ -872,9 +872,9 @@ function textAnswer(
description={() => (
<box gap={1}>
<Show when={field.description}>
{(description) => <text fg={theme.text.subdued}>{description()}</text>}
{(description) => <text fg={theme.text.muted}>{description()}</text>}
</Show>
<Show when={error()}>{(value) => <text fg={theme.text.feedback.error.default}>{value()}</text>}</Show>
<Show when={error()}>{(value) => <text fg={theme.text.feedback.error.base}>{value()}</text>}</Show>
</box>
)}
/>
+6 -6
View File
@@ -56,10 +56,10 @@ export function DialogMcp(props: { initialServer?: string; details?: boolean } =
const [loading, setLoading] = createSignal<ReadonlySet<string>>(new Set())
const statusColor = (status: McpServer["status"]) => {
if (status.status === "connected") return theme.text.feedback.success.default
if (status.status === "failed") return theme.text.feedback.error.default
if (status.status === "needs_auth") return theme.text.feedback.warning.default
return theme.text.subdued
if (status.status === "connected") return theme.text.feedback.success.base
if (status.status === "failed") return theme.text.feedback.error.base
if (status.status === "needs_auth") return theme.text.feedback.warning.base
return theme.text.muted
}
createEffect(() => {
@@ -76,7 +76,7 @@ export function DialogMcp(props: { initialServer?: string; details?: boolean } =
value: server.name,
title: server.name,
footer: <Status status={server.status} loading={pending} />,
footerColor: pending ? theme.text.subdued : statusColor(server.status),
footerColor: pending ? theme.text.muted : statusColor(server.status),
}
})
})
@@ -155,7 +155,7 @@ export function DialogMcp(props: { initialServer?: string; details?: boolean } =
]}
footer={
<Show when={focusedError()}>
<text fg={theme.text.subdued}>enter to view error</text>
<text fg={theme.text.muted}>enter to view error</text>
</Show>
}
/>
+6 -6
View File
@@ -336,7 +336,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
emptyView={
<Show when={!recent.loading && !projects.loading}>
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No recent sessions or projects</text>
<text fg={theme.text.muted}>No recent sessions or projects</text>
</box>
</Show>
}
@@ -350,13 +350,13 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
>
<box>
<Show when={projectID() && worktrees.loading}>
<Spinner color={theme.text.subdued}>Loading worktrees</Spinner>
<Spinner color={theme.text.muted}>Loading worktrees</Spinner>
</Show>
<Show when={!projectID() && (recent.loading || projects.loading)}>
<Spinner color={theme.text.subdued}>Refreshing sessions and projects</Spinner>
<Spinner color={theme.text.muted}>Refreshing sessions and projects</Spinner>
</Show>
<Show when={!projectID() && (recent() === false || projects() === false)}>
<text fg={theme.text.feedback.error.default}>
<text fg={theme.text.feedback.error.base}>
Could not refresh{" "}
{recent() === false ? (projects() === false ? "sessions and projects" : "sessions") : "projects"}.
</text>
@@ -402,7 +402,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
footerHints={[...(projectID() ? [{ title: "new worktree", label: "ctrl+n" }] : [])]}
noMatchView={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{projectID()
? worktrees.loading
? "Loading worktrees…"
@@ -434,7 +434,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
size="large"
title={`${projectName(data.project.get(projectID()!)) ?? "Project"} / New worktree`}
placeholder="Worktree name (optional)"
description={() => <text fg={theme.text.subdued}>Leave blank for a random name.</text>}
description={() => <text fg={theme.text.muted}>Leave blank for a random name.</text>}
busy={creating()}
busyText="Creating worktree…"
onCancel={cancelCreation}
+16 -16
View File
@@ -61,33 +61,33 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
<box flexDirection={horizontal() ? "row" : "column"} alignItems={horizontal() ? "flex-start" : "center"} gap={2}>
<box width={horizontal() ? 29 : "100%"} flexShrink={0} gap={1}>
<box>
<text fg={theme.text.subdued}>This device</text>
<text fg={theme.text.muted}>This device</text>
<Show when={localhost()}>
{(url) => (
<Link href={href(url())} fg={theme.text.default}>
<Link href={href(url())} fg={theme.text.base}>
{url()}
</Link>
)}
</Show>
</box>
<box>
<text fg={theme.text.subdued}>URLs</text>
<text fg={theme.text.muted}>URLs</text>
<For each={value.urls}>
{(url) => (
<Link href={href(url)} fg={theme.text.default}>
<Link href={href(url)} fg={theme.text.base}>
{url}
</Link>
)}
</For>
</box>
<box>
<text fg={theme.text.subdued}>Username</text>
<text fg={theme.text.default}>{value.username}</text>
<text fg={theme.text.muted}>Username</text>
<text fg={theme.text.base}>{value.username}</text>
</box>
<box>
<text fg={theme.text.subdued}>Password</text>
<text fg={theme.text.muted}>Password</text>
<text
fg={passwordHover() ? theme.text.default : theme.text.subdued}
fg={passwordHover() ? theme.text.base : theme.text.muted}
wrapMode="word"
onMouseOver={() => setPasswordHover(true)}
onMouseOut={() => setPasswordHover(false)}
@@ -97,7 +97,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
</text>
</box>
<Show when={value.urls.some((url) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname))}>
<text fg={theme.text.subdued} wrapMode="word">
<text fg={theme.text.muted} wrapMode="word">
Run `opencode service set hostname 0.0.0.0` to access the service remotely.
</text>
</Show>
@@ -108,7 +108,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
flexShrink={0}
alignItems={horizontal() ? "flex-end" : "center"}
>
<text fg={theme.text.default}>{renderUnicodeCompact(JSON.stringify(value), { border: 1 })}</text>
<text fg={theme.text.base}>{renderUnicodeCompact(JSON.stringify(value), { border: 1 })}</text>
</box>
</box>
)
@@ -117,17 +117,17 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
return (
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
Pair
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<Show
when={loadError()}
fallback={
<Show when={info()} fallback={<text fg={theme.text.subdued}>Loading server information</text>}>
<Show when={info()} fallback={<text fg={theme.text.muted}>Loading server information</text>}>
<Show
when={dimensions().height >= 36}
fallback={
@@ -146,11 +146,11 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
>
{(error) => (
<box>
<text fg={theme.text.feedback.error.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.feedback.error.base} attributes={TextAttributes.BOLD}>
Could not load server information
</text>
<text fg={theme.text.subdued}>{errorMessage(error())}</text>
<text fg={theme.text.subdued}>Close and reopen Pair to try again.</text>
<text fg={theme.text.muted}>{errorMessage(error())}</text>
<text fg={theme.text.muted}>Close and reopen Pair to try again.</text>
</box>
)}
</Show>
@@ -197,11 +197,11 @@ export function DialogSessionList() {
title="Sessions"
titleView={
<box flexDirection="row">
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
Sessions
</text>
<Show when={!allProjects() && currentProjectName()}>
<text fg={theme.text.subdued}> for {currentProjectName()}</text>
<text fg={theme.text.muted}> for {currentProjectName()}</text>
</Show>
</box>
}
@@ -226,14 +226,14 @@ export function DialogSessionList() {
]}
emptyView={
<box paddingLeft={4} paddingRight={4}>
<text fg={searchState().error ? theme.text.feedback.error.default : theme.text.subdued}>
<text fg={searchState().error ? theme.text.feedback.error.base : theme.text.muted}>
{searchState().message}
</text>
</box>
}
noMatchView={
<box paddingLeft={4} paddingRight={4}>
<text fg={searchState().error ? theme.text.feedback.error.default : theme.text.subdued}>
<text fg={searchState().error ? theme.text.feedback.error.base : theme.text.muted}>
{searchState().message}
</text>
</box>
@@ -102,19 +102,19 @@ export function DialogShellOutput(props: { shell: ShellInfo; location: LocationR
return (
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" gap={2}>
<text fg={theme.text.default} attributes={TextAttributes.BOLD} flexGrow={1}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD} flexGrow={1}>
Shell output
</text>
<text fg={theme.text.subdued}>{status()}</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted}>{status()}</text>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<text fg={theme.text.subdued} maxHeight={3} wrapMode="word">
<text fg={theme.text.muted} maxHeight={3} wrapMode="word">
{props.shell.command}
</text>
<Show when={omitted()}>
<text fg={theme.text.subdued}>Earlier output omitted · showing recent output</text>
<text fg={theme.text.muted}>Earlier output omitted · showing recent output</text>
</Show>
<scrollbox
id="shell-output-scroll"
@@ -124,7 +124,7 @@ export function DialogShellOutput(props: { shell: ShellInfo; location: LocationR
stickyStart="bottom"
scrollbarOptions={{ visible: false }}
>
<text fg={theme.text.default} wrapMode="word">
<text fg={theme.text.base} wrapMode="word">
{text() ||
(output() === undefined
? "Loading output…"
@@ -132,12 +132,12 @@ export function DialogShellOutput(props: { shell: ShellInfo; location: LocationR
</text>
</scrollbox>
<Show when={error()}>
<text fg={theme.text.feedback.error.default}>{error()}</text>
<text fg={theme.text.feedback.error.base}>{error()}</text>
</Show>
<box flexDirection="row" gap={2} flexWrap="wrap">
<text fg={theme.text.subdued}>/ scroll</text>
<text fg={theme.text.subdued}>end follow</text>
<text fg={theme.text.subdued}>esc back</text>
<text fg={theme.text.muted}>/ scroll</text>
<text fg={theme.text.muted}>end follow</text>
<text fg={theme.text.muted}>esc back</text>
</box>
</box>
)
+6 -6
View File
@@ -63,29 +63,29 @@ export function DialogSkill(props: DialogSkillProps) {
<Switch
fallback={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No skills available</text>
<text fg={theme.text.muted}>No skills available</text>
</box>
}
>
<Match when={showError()}>
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.feedback.error.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.feedback.error.base} attributes={TextAttributes.BOLD}>
Could not load skills
</text>
<text fg={theme.text.subdued}>{errorMessage(loadError())}</text>
<text fg={theme.text.subdued}>Close and reopen Skills to try again.</text>
<text fg={theme.text.muted}>{errorMessage(loadError())}</text>
<text fg={theme.text.muted}>Close and reopen Skills to try again.</text>
</box>
</Match>
<Match when={skills.loading}>
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>Loading skills</text>
<text fg={theme.text.muted}>Loading skills</text>
</box>
</Match>
</Switch>
}
noMatchView={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No skills found</text>
<text fg={theme.text.muted}>No skills found</text>
</box>
}
/>
+10 -10
View File
@@ -11,24 +11,24 @@ export function DialogStatus() {
const mcp = createMemo(() => data.location.mcp.server.list() ?? [])
const color = (status: string) => {
if (status === "connected") return theme.text.feedback.success.default
if (status === "failed") return theme.text.feedback.error.default
if (status === "needs_auth") return theme.text.feedback.warning.default
return theme.text.subdued
if (status === "connected") return theme.text.feedback.success.base
if (status === "failed") return theme.text.feedback.error.base
if (status === "needs_auth") return theme.text.feedback.warning.base
return theme.text.muted
}
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
Status
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<Show when={mcp().length > 0} fallback={<text fg={theme.text.default}>No MCP servers</text>}>
<Show when={mcp().length > 0} fallback={<text fg={theme.text.base}>No MCP servers</text>}>
<box>
<text fg={theme.text.default}>
<text fg={theme.text.base}>
{mcp().length} MCP server{mcp().length === 1 ? "" : "s"}
</text>
<For each={mcp()}>
@@ -37,9 +37,9 @@ export function DialogStatus() {
<text flexShrink={0} style={{ fg: color(item.status.status) }}>
</text>
<text fg={theme.text.default} wrapMode="word">
<text fg={theme.text.base} wrapMode="word">
<b>{item.name}</b>{" "}
<span style={{ fg: theme.text.subdued }}>
<span style={{ fg: theme.text.muted }}>
<Switch fallback={item.status.status}>
<Match when={item.status.status === "connected"}>Connected</Match>
<Match when={item.status.status === "failed" && item.status}>{(val) => val().error}</Match>
+10 -10
View File
@@ -88,14 +88,14 @@ export function DialogUpdate(props: {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{state().type === "installing"
? "Updating OpenCode"
: state().type === "available" || state().type === "failed"
? "Update available"
: "Update"}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -104,33 +104,33 @@ export function DialogUpdate(props: {
{(current) => (
<Switch>
<Match when={current.type === "checking"}>
<Spinner shimmer={theme.text.default}>Checking for updates</Spinner>
<Spinner shimmer={theme.text.base}>Checking for updates</Spinner>
</Match>
<Match when={current.type === "available"}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
An update is available. After installing, you'll be prompted to restart OpenCode.
</text>
</Match>
<Match when={current.type === "installing"}>
<Spinner shimmer={theme.text.default}>
<Spinner shimmer={theme.text.base}>
{current.type === "installing" ? `Installing OpenCode ${current.version}` : ""}
</Spinner>
</Match>
<Match when={current.type === "installed"}>
<text fg={theme.text.subdued} wrapMode="word">
<text fg={theme.text.muted} wrapMode="word">
Update successful! A restart is required. Any active sessions will be resumed automatically.
</text>
</Match>
<Match when={current.type === "current"}>
<text fg={theme.text.subdued}>OpenCode is already up to date.</text>
<text fg={theme.text.muted}>OpenCode is already up to date.</text>
</Match>
<Match when={current.type === "unavailable"}>
<text fg={theme.text.subdued} wrapMode="word">
<text fg={theme.text.muted} wrapMode="word">
{current.type === "unavailable" ? current.message : ""}
</text>
</Match>
<Match when={current.type === "failed" || current.type === "check-failed"}>
<text fg={theme.text.feedback.error.default}>
<text fg={theme.text.feedback.error.base}>
{current.type === "failed" || current.type === "check-failed" ? current.message : ""}
</text>
</Match>
@@ -148,7 +148,7 @@ export function DialogUpdate(props: {
backgroundColor={active() === index() ? theme.background.action.primary.focused : undefined}
onMouseUp={() => void button.run()}
>
<text fg={active() === index() ? theme.text.action.primary.focused : theme.text.subdued}>
<text fg={active() === index() ? theme.text.action.primary.focused : theme.text.muted}>
{button.label}
</text>
</box>
@@ -72,15 +72,15 @@ export function DialogWorkspaceFileChanges(props: {
return (
<box gap={1}>
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{props.title ?? "File Changes Found"}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingLeft={2} paddingRight={2}>
<text fg={theme.text.subdued} wrapMode="word">
<text fg={theme.text.muted} wrapMode="word">
{props.message ?? "Do you want to move these changes with the session?"}
</text>
</box>
@@ -95,9 +95,9 @@ export function DialogWorkspaceFileChanges(props: {
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
<box flexDirection="row" minWidth={0} flexShrink={1}>
<box width={2} flexShrink={0}>
<text fg={overlayTheme.text.subdued}>{statusLabel(item.status)}</text>
<text fg={overlayTheme.text.muted}>{statusLabel(item.status)}</text>
</box>
<FilePath value={item.file} maxWidth={fileNameWidth()} fg={overlayTheme.text.subdued} />
<FilePath value={item.file} maxWidth={fileNameWidth()} fg={overlayTheme.text.muted} />
</box>
<box flexDirection="row" gap={1} minWidth={7} flexShrink={0} justifyContent="flex-end">
<text>
@@ -125,7 +125,7 @@ export function DialogWorkspaceFileChanges(props: {
dialog.clear()
}}
>
<text fg={item === store.active ? theme.text.action.primary.focused : theme.text.subdued}>{item}</text>
<text fg={item === store.active ? theme.text.action.primary.focused : theme.text.muted}>{item}</text>
</box>
)}
</For>
@@ -175,18 +175,18 @@ export function DialogWorkspaces(props: DialogWorkspacesProps) {
return {
title,
titleView: isRemoving ? (
<span style={{ fg: theme.text.feedback.error.default }}>Deleting {item.location}</span>
<span style={{ fg: theme.text.feedback.error.base }}>Deleting {item.location}</span>
) : deleting ? (
<span style={{ fg: theme.text.action.destructive.default }}>
<span style={{ fg: theme.text.action.destructive.base }}>
Press {shortcuts.get("dialog.move_session.delete")} again to confirm
</span>
) : suffix ? (
<>
{visible.slice(0, split)}
<span style={{ fg: theme.text.subdued }}>{visible.slice(split)}</span>
<span style={{ fg: theme.text.muted }}>{visible.slice(split)}</span>
</>
) : undefined,
bg: deleting ? theme.background.action.destructive.default : undefined,
bg: deleting ? theme.background.action.destructive.base : undefined,
value: {
type: "directory",
directory: item.location,
@@ -327,7 +327,7 @@ export function DialogWorkspaces(props: DialogWorkspacesProps) {
title="Worktrees"
titleView={
<box flexDirection="row" gap={1}>
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
Worktrees
</text>
<Show when={working() || directories.loading || loadedProject.loading}>
@@ -341,25 +341,25 @@ export function DialogWorkspaces(props: DialogWorkspacesProps) {
emptyView={
showError() ? (
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.feedback.error.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.feedback.error.base} attributes={TextAttributes.BOLD}>
Could not load worktrees
</text>
<text fg={theme.text.subdued}>{errorMessage(loadError())}</text>
<text fg={theme.text.subdued}>Close and reopen Worktrees to try again.</text>
<text fg={theme.text.muted}>{errorMessage(loadError())}</text>
<text fg={theme.text.muted}>Close and reopen Worktrees to try again.</text>
</box>
) : directories.loading || loadedProject.loading ? (
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>Loading worktrees</text>
<text fg={theme.text.muted}>Loading worktrees</text>
</box>
) : (
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No worktrees available</text>
<text fg={theme.text.muted}>No worktrees available</text>
</box>
)
}
noMatchView={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No worktrees found</text>
<text fg={theme.text.muted}>No worktrees found</text>
</box>
}
locked={showError() || directories.loading || loadedProject.loading || Boolean(removing())}
@@ -49,10 +49,10 @@ export function DialogWorktreeName(props: { onConfirm: (name: string) => void })
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
Name worktree
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -64,17 +64,17 @@ export function DialogWorktreeName(props: { onConfirm: (name: string) => void })
}}
onSubmit={confirm}
placeholder="Worktree name"
placeholderColor={theme.text.subdued}
textColor={theme.text.formfield.default}
focusedTextColor={theme.text.formfield.default}
cursorColor={theme.text.formfield.default}
placeholderColor={theme.text.muted}
textColor={theme.text.formfield.base}
focusedTextColor={theme.text.formfield.base}
cursorColor={theme.text.formfield.base}
/>
<box paddingBottom={1} flexDirection="row" gap={2}>
<text fg={theme.text.default}>
enter <span style={{ fg: theme.text.subdued }}>submit</span>
<text fg={theme.text.base}>
enter <span style={{ fg: theme.text.muted }}>submit</span>
</text>
<text fg={theme.text.default}>
{shortcuts.get("dialog.worktree.generate")} <span style={{ fg: theme.text.subdued }}>generate one</span>
<text fg={theme.text.base}>
{shortcuts.get("dialog.worktree.generate")} <span style={{ fg: theme.text.muted }}>generate one</span>
</text>
</box>
</box>
+6 -6
View File
@@ -10,7 +10,7 @@ export function Logo() {
const dimensions = useTerminalDimensions()
const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
const shadow = tint(theme.background.default, fg, 0.25)
const shadow = tint(theme.background.base, fg, 0.25)
const attrs = bold ? TextAttributes.BOLD : undefined
return Array.from(line).map((char) => {
if (char === "_") {
@@ -53,23 +53,23 @@ export function Logo() {
<box>
{dimensions().height < 12 ? null : dimensions().width < 22 ? (
<For each={go.right.slice(1)}>
{(line) => <box flexDirection="row">{renderLine(line, theme.text.default, true)}</box>}
{(line) => <box flexDirection="row">{renderLine(line, theme.text.base, true)}</box>}
</For>
) : dimensions().width < 44 ? (
<>
<For each={logo.left.slice(1)}>
{(line) => <box flexDirection="row">{renderLine(line, theme.text.subdued, false)}</box>}
{(line) => <box flexDirection="row">{renderLine(line, theme.text.muted, false)}</box>}
</For>
<For each={logo.right}>
{(line) => <box flexDirection="row">{renderLine(line, theme.text.default, true)}</box>}
{(line) => <box flexDirection="row">{renderLine(line, theme.text.base, true)}</box>}
</For>
</>
) : (
<For each={logo.left}>
{(line, index) => (
<box flexDirection="row" gap={1}>
<box flexDirection="row">{renderLine(line, theme.text.subdued, false)}</box>
<box flexDirection="row">{renderLine(logo.right[index()], theme.text.default, true)}</box>
<box flexDirection="row">{renderLine(line, theme.text.muted, false)}</box>
<box flexDirection="row">{renderLine(logo.right[index()], theme.text.base, true)}</box>
</box>
)}
</For>
@@ -54,14 +54,14 @@ export function MigrationOverlay() {
flexDirection="row"
backgroundColor={theme.background.raised.high}
border={["left"]}
borderColor={theme.text.feedback.info.default}
borderColor={theme.text.feedback.info.base}
customBorderChars={SplitBorder.customBorderChars}
paddingLeft={2}
paddingRight={2}
paddingTop={1}
paddingBottom={1}
>
<Spinner color={theme.text.feedback.info.default}>
<Spinner color={theme.text.feedback.info.base}>
{value().label}
{count(value())}
</Spinner>
+1 -1
View File
@@ -21,7 +21,7 @@ export function PanelHost(props: {
const theme = useTheme()
// Side panels sit on a raised surface; fullscreen takes over the base background.
const background = () =>
panels.presentation() === "panel" ? theme.background.raised.base : theme.background.default
panels.presentation() === "panel" ? theme.background.raised.base : theme.background.base
return (
<box
id="session-panel"
@@ -5,7 +5,7 @@ export function PluginRouteMissing(props: { id: string; name: string; onHome: ()
return (
<box width="100%" height="100%" alignItems="center" justifyContent="center" flexDirection="column" gap={1}>
<text fg={theme.text.feedback.warning.default}>
<text fg={theme.text.feedback.warning.base}>
Unknown plugin route: {props.id}/{props.name}
</text>
<box
@@ -879,7 +879,7 @@ export function Autocomplete(props: {
width={position().width}
zIndex={100}
{...SplitBorder}
borderColor={theme.border.default}
borderColor={theme.border.base}
>
<scrollbox
ref={(r: ScrollBoxRenderable) => {
@@ -896,7 +896,7 @@ export function Autocomplete(props: {
each={options()}
fallback={
<box paddingLeft={1} paddingRight={1}>
<text fg={emptyError() ? theme.text.feedback.error.default : theme.text.subdued}>{emptyMessage()}</text>
<text fg={emptyError() ? theme.text.feedback.error.base : theme.text.muted}>{emptyMessage()}</text>
</box>
}
>
@@ -928,7 +928,7 @@ export function Autocomplete(props: {
? theme.text.action.destructive.focused
: index === store.selected
? theme.text.action.primary.focused
: theme.text.default
: theme.text.base
}
flexShrink={0}
>
@@ -936,7 +936,7 @@ export function Autocomplete(props: {
</text>
<Show when={!confirmingAction() && option().description}>
<text
fg={index === store.selected ? theme.text.action.primary.focused : theme.text.subdued}
fg={index === store.selected ? theme.text.action.primary.focused : theme.text.muted}
wrapMode="none"
>
{" " + option().description?.replace(/\s+/g, " ").trim()}
+20 -20
View File
@@ -350,7 +350,7 @@ export function Prompt(props: PromptProps) {
createEffect(() => {
if (!input || input.isDestroyed) return
input.cursorColor = disabled() ? theme.background.raised.base : theme.text.default
input.cursorColor = disabled() ? theme.background.raised.base : theme.text.base
if (config.cursor) input.cursorStyle = config.cursor
})
@@ -1554,9 +1554,9 @@ export function Prompt(props: PromptProps) {
},
)
const highlight = createMemo(() => {
if (muted()) return theme.border.default
if (muted()) return theme.border.base
if (store.mode === "shell") return theme.text.action.primary.selected
return promptDisplay().agentColor ?? theme.border.default
return promptDisplay().agentColor ?? theme.border.base
})
const agentLabel = createMemo(() => (store.mode === "shell" ? "Shell" : promptDisplay().agentLabel))
const animateMetadata = !revealedPromptMetadata.has(local)
@@ -1573,7 +1573,7 @@ export function Prompt(props: PromptProps) {
createEffect(() => {
if (agentLabel()) revealedPromptMetadata.add(local)
})
const borderHighlight = createMemo(() => tint(theme.border.default, highlight(), agentMetaAlpha()))
const borderHighlight = createMemo(() => tint(theme.border.base, highlight(), agentMetaAlpha()))
const footerInput = () => ({
sessionID: props.sessionID,
mode: store.mode,
@@ -1622,7 +1622,7 @@ export function Prompt(props: PromptProps) {
})
const spinnerDef = createMemo(() => {
const color = promptDisplay().agentColor ?? theme.border.default
const color = promptDisplay().agentColor ?? theme.border.base
return {
frames: createFrames({
color,
@@ -1694,7 +1694,7 @@ export function Prompt(props: PromptProps) {
when={!failed()}
fallback={
<box width="100%" height="100%" alignItems="center" justifyContent="center">
<text fg={theme.text.subdued}>No preview</text>
<text fg={theme.text.muted}>No preview</text>
</box>
}
>
@@ -1726,7 +1726,7 @@ export function Prompt(props: PromptProps) {
openImagePreview(visibleImageAttachments().length)
}}
>
<text fg={theme.text.subdued} wrapMode="none" truncate>
<text fg={theme.text.muted} wrapMode="none" truncate>
+{imageAttachments().length - visibleImageAttachments().length} more
</text>
</box>
@@ -1736,9 +1736,9 @@ export function Prompt(props: PromptProps) {
<textarea
width="100%"
placeholder={placeholderText()}
placeholderColor={theme.text.subdued}
textColor={muted() ? theme.text.subdued : theme.text.default}
focusedTextColor={muted() ? theme.text.subdued : theme.text.default}
placeholderColor={theme.text.muted}
textColor={muted() ? theme.text.muted : theme.text.base}
focusedTextColor={muted() ? theme.text.muted : theme.text.base}
minHeight={1}
maxHeight={maxHeight()}
cursorStyle={config.cursor}
@@ -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.raised.base : theme.text.default
input.cursorColor = disabled() ? theme.background.raised.base : theme.text.base
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.raised.base : theme.text.default}
cursorColor={disabled() ? theme.background.raised.base : theme.text.base}
syntaxStyle={syntax()}
/>
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1} justifyContent="space-between">
@@ -1885,17 +1885,17 @@ export function Prompt(props: PromptProps) {
<Match when={status() === "running"}>
<box flexDirection="row" gap={1} flexGrow={1} justifyContent="flex-start">
<box marginLeft={1}>
<Show when={config.animations ?? true} fallback={<text fg={theme.text.subdued}>[]</text>}>
<Show when={config.animations ?? true} fallback={<text fg={theme.text.muted}>[]</text>}>
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
</Show>
</box>
<PromptInterruptStatus
armed={store.interrupt > 0}
animations={animationsEnabled()}
text={theme.text.default}
subdued={theme.text.subdued}
warning={theme.text.feedback.warning.default}
flash={theme.decrease(theme.text.feedback.warning.default, 2)}
text={theme.text.base}
subdued={theme.text.muted}
warning={theme.text.feedback.warning.base}
flash={theme.decrease(theme.text.feedback.warning.base, 2)}
/>
</box>
</Match>
@@ -1904,7 +1904,7 @@ export function Prompt(props: PromptProps) {
<box paddingLeft={3} height={1} minHeight={0} flexShrink={1}>
<Spinner color={theme.hue.accent[500]}>
{progress()}
<span style={{ fg: theme.text.subdued }}>{".".repeat(move.creatingDots())}</span>
<span style={{ fg: theme.text.muted }}>{".".repeat(move.creatingDots())}</span>
</Spinner>
</box>
)}
@@ -1921,7 +1921,7 @@ export function Prompt(props: PromptProps) {
{(location) => (
<text
id="prompt.footer.location"
fg={locationActions.hovered() ? theme.text.default : theme.text.subdued}
fg={locationActions.hovered() ? theme.text.base : theme.text.muted}
wrapMode="none"
truncate
flexGrow={1}
@@ -1945,7 +1945,7 @@ export function Prompt(props: PromptProps) {
wrapMode="none"
truncate
flexShrink={1}
fg={editorContextLabelState() === "pending" ? theme.hue.accent[500] : theme.text.subdued}
fg={editorContextLabelState() === "pending" ? theme.hue.accent[500] : theme.text.muted}
>
{file()}
</text>
@@ -43,25 +43,25 @@ export function PromptMetadataRow(props: {
{(agent) => <text fg={fade(props.highlight, props.agentAlpha)}>{agent()}</text>}
</Show>
<Show when={props.mode === "normal" && layout().auto}>
<text fg={fade(theme.text.subdued, props.agentAlpha)}>auto</text>
<text fg={fade(theme.text.muted, props.agentAlpha)}>auto</text>
</Show>
<Show when={props.mode === "normal" && layout().model}>
<box flexDirection="row" gap={1} flexGrow={1} flexShrink={1} minWidth={0}>
<Show when={layout().agent}>
<text fg={fade(theme.text.subdued, props.modelAlpha)}>·</text>
<text fg={fade(theme.text.muted, props.modelAlpha)}>·</text>
</Show>
<text
flexShrink={1}
minWidth={0}
wrapMode="none"
truncate
fg={fade(props.muted ? theme.text.subdued : theme.text.default, props.modelAlpha)}
fg={fade(props.muted ? theme.text.muted : theme.text.base, props.modelAlpha)}
>
{layout().model}
</text>
<Show when={layout().provider}>
{(provider) => (
<text flexShrink={0} fg={fade(theme.text.subdued, props.modelAlpha)}>
<text flexShrink={0} fg={fade(theme.text.muted, props.modelAlpha)}>
{provider()}
</text>
)}
@@ -69,9 +69,9 @@ export function PromptMetadataRow(props: {
<Show when={layout().variant}>
{(variant) => (
<>
<text fg={fade(theme.text.subdued, props.variantAlpha)}>·</text>
<text fg={fade(theme.text.muted, props.variantAlpha)}>·</text>
<text
fg={fade(theme.text.feedback.warning.default, props.variantAlpha)}
fg={fade(theme.text.feedback.warning.base, props.variantAlpha)}
attributes={TextAttributes.BOLD}
>
{variant()}
+2 -2
View File
@@ -28,8 +28,8 @@ export function Reconnecting(props: { managed?: boolean }) {
paddingRight={2}
gap={1}
>
<Spinner color={theme.text.default}>{props.managed ? "Restarting service…" : "Connection lost…"}</Spinner>
<text fg={theme.text.subdued}>
<Spinner color={theme.text.base}>{props.managed ? "Restarting service…" : "Connection lost…"}</Spinner>
<text fg={theme.text.muted}>
{props.managed
? "Your session will resume automatically."
: "Reconnecting to the server automatically."}
@@ -47,7 +47,7 @@ export function SessionTabsRailControls(props: {
}}
onMouseDragEnd={() => (pressed = false)}
>
<text width={1} height={1} fg={theme.text.action.secondary.default} selectable={false} wrapMode="none">
<text width={1} height={1} fg={theme.text.action.secondary.base} selectable={false} wrapMode="none">
</text>
</box>
+51 -51
View File
@@ -120,8 +120,8 @@ const glowTextColor = (base: RGBA, glow: RGBA, index: number, width: number, lev
tint(base, glow, 0.12 * unreadGlowIntensity(index, width) * level)
function tabFeedbackColor(status: SessionTabsStatus, theme: ReturnType<typeof useTheme>) {
if (status.attention) return theme.text.status[status.attention]
if (status.unread === "error") return theme.text.feedback.error.default
if (status.attention) return theme.hue.interactive[800]
if (status.unread === "error") return theme.text.feedback.error.base
return undefined
}
@@ -455,7 +455,7 @@ function TabContextMenu(props: { state: TabContextMenuState; tabs: SessionTabsCo
run(index())
}}
>
<text fg={theme.text.default} selectable={false}>
<text fg={theme.text.base} selectable={false}>
{action.title}
</text>
</box>
@@ -528,11 +528,11 @@ function VerticalSessionTabs(props: {
const compact = createMemo(() => width() < SESSION_TABS_COMPACT_BREAKPOINT)
const tooltipWidth = () => Math.min(54, dimensions().width - width())
const stride = () => (compact() ? 2 : 3)
const unreadColor = () => theme.text.status.unread
const activeNumber = () => theme.text.status.running
const idleNumber = () => tint(theme.text.formfield.default, background(), 0.55)
const separatorUpperPulseColor = createMemo(() => tint(background(), theme.text.default, 0.04))
const separatorLowerPulseColor = createMemo(() => tint(background(), theme.text.default, 0.05))
const unreadColor = () => theme.hue.interactive[800]
const activeNumber = () => theme.hue.accent[800]
const idleNumber = () => tint(theme.text.formfield.base, background(), 0.55)
const separatorUpperPulseColor = createMemo(() => tint(background(), theme.text.base, 0.04))
const separatorLowerPulseColor = createMemo(() => tint(background(), theme.text.base, 0.05))
const [addHovered, setAddHovered] = createSignal(false)
const marquee = createTabMarquee(animations)
const hovered = marquee.hovered
@@ -750,16 +750,16 @@ function VerticalSessionTabs(props: {
const base =
hovered() === tab.sessionID && !selected()
? foreground()
: tint(idleNumber(), tint(theme.text.default, pulseBackground(), 0.25), Number(selected()))
: tint(idleNumber(), tint(theme.text.base, pulseBackground(), 0.25), Number(selected()))
const color = tabFeedbackColor(status(), theme) ?? tint(base, glowHue(), numberGlow.value().level)
const runningColor = runs() ? activeNumber() : color
return sweepLevel() === 0
? tint(runningColor, theme.text.default, numberIgnition.value().level)
: tint(runningColor, theme.text.default, Math.max(numberIgnition.value().level, 0.35 * sweepLevel()))
? tint(runningColor, theme.text.base, numberIgnition.value().level)
: tint(runningColor, theme.text.base, Math.max(numberIgnition.value().level, 0.35 * sweepLevel()))
}
const foreground = () => {
if (hovered() === tab.sessionID) return theme.text.default
return selected() ? theme.text.default : theme.text.subdued
if (hovered() === tab.sessionID) return theme.text.base
return selected() ? theme.text.base : theme.text.muted
}
const complete = () => status().complete
// Latched so a resolving glow fades out in the hue it lit with instead of snapping to the unread color.
@@ -770,14 +770,14 @@ function VerticalSessionTabs(props: {
if (status().unread !== undefined) return (lastGlowHue = unreadColor())
return lastGlowHue ?? unreadColor()
}
const pulseColor = createMemo(() => tint(pulseBackground(), theme.text.default, 0.25))
const flashColor = createMemo(() => tint(pulseBackground(), theme.text.default, 0.7))
const pulseColor = createMemo(() => tint(pulseBackground(), theme.text.base, 0.25))
const flashColor = createMemo(() => tint(pulseBackground(), theme.text.base, 0.7))
const glowLevel = createGlowLevel(() => selected() && Boolean(status().attention), animations)
const glowColor = createMemo(() => tint(pulseBackground(), glowHue(), 0.45 * glowLevel()))
const detailPulseColor = createMemo(() => tint(pulseBackground(), theme.text.default, 0.13))
const detailFlashColor = createMemo(() => tint(pulseBackground(), theme.text.default, 0.42))
const detailPulseColor = createMemo(() => tint(pulseBackground(), theme.text.base, 0.13))
const detailFlashColor = createMemo(() => tint(pulseBackground(), theme.text.base, 0.42))
const detailGlowColor = createMemo(() => tint(pulseBackground(), glowHue(), 0.25 * glowLevel()))
const detailColor = createMemo(() => tint(theme.text.subdued, pulseBackground(), 0.35))
const detailColor = createMemo(() => tint(theme.text.muted, pulseBackground(), 0.35))
const detailTextColor = (index: number) =>
detailFades()
? fadeTitleColor(detailColor(), pulseBackground(), index, visibleDetailParts().length, 0)
@@ -919,14 +919,14 @@ function VerticalSessionTabs(props: {
idleLabel={Locale.graphemes(title().trimStart())[0] ?? "U"}
color={
selected()
? theme.text.default
? theme.text.base
: props.numbers
? numberColor()
: (tabFeedbackColor(status(), theme) ?? (runs() ? activeNumber() : foreground()))
}
unreadColor={tabFeedbackColor(status(), theme) ?? unreadColor()}
backgroundColor={pulseBackground()}
flashColor={theme.text.default}
flashColor={theme.text.base}
animations={animations()}
numbers={props.numbers}
spinner={props.spinner}
@@ -951,8 +951,8 @@ function VerticalSessionTabs(props: {
color={separatorLowerPulseColor()}
width={indicatorWidth}
outerColor={separatorUpperPulseColor()}
flashColor={tint(background(), theme.text.default, 0.22)}
outerFlashColor={tint(background(), theme.text.default, 0.18)}
flashColor={tint(background(), theme.text.base, 0.22)}
outerFlashColor={tint(background(), theme.text.base, 0.18)}
flashTail={8}
glowColor={separatorLowerColor()}
outerGlowColor={separatorUpperColor()}
@@ -975,10 +975,10 @@ function VerticalSessionTabs(props: {
outerComplete={false}
glow={glows()}
outerGlow={false}
color={tint(background(), theme.text.default, 0.04)}
color={tint(background(), theme.text.base, 0.04)}
width={indicatorWidth}
outerColor={tint(background(), theme.text.default, 0.006)}
flashColor={tint(background(), theme.text.default, 0.18)}
outerColor={tint(background(), theme.text.base, 0.006)}
flashColor={tint(background(), theme.text.base, 0.18)}
flashTail={8}
glowColor={tint(background(), glowHue(), 0.1 * glowLevel())}
outerGlowColor={background()}
@@ -1013,7 +1013,7 @@ function VerticalSessionTabs(props: {
color={numberColor()}
unreadColor={tabFeedbackColor(status(), theme) ?? unreadColor()}
backgroundColor={pulseBackground()}
flashColor={theme.text.default}
flashColor={theme.text.base}
animations={animations()}
numbers={props.numbers}
spinner={props.spinner}
@@ -1053,7 +1053,7 @@ function VerticalSessionTabs(props: {
right={1}
zIndex={2}
width={1}
fg={closeHovered() ? theme.text.default : theme.text.subdued}
fg={closeHovered() ? theme.text.base : theme.text.muted}
selectable={false}
onMouseOver={() => setCloseHovered(true)}
onMouseOut={() => setCloseHovered(false)}
@@ -1105,7 +1105,7 @@ function VerticalSessionTabs(props: {
)
}}
</For>
{/* One slot with two states: a subdued affordance that promotes in place into the
{/* One slot with two states: a muted affordance that promotes in place into the
active new-session tab, instead of spawning a separate pseudo tab above itself. */}
<Show when={tabs.add || newTab()}>
<box
@@ -1162,7 +1162,7 @@ function VerticalSessionTabs(props: {
</Show>
<text
width={compact() ? 1 : 2}
fg={newTab() || addHovered() ? theme.text.default : idleNumber()}
fg={newTab() || addHovered() ? theme.text.base : idleNumber()}
selectable={false}
attributes={newTab() ? TextAttributes.BOLD : undefined}
>
@@ -1170,7 +1170,7 @@ function VerticalSessionTabs(props: {
</text>
<Show when={!compact()}>
<text
fg={newTab() || addHovered() ? theme.text.default : theme.text.subdued}
fg={newTab() || addHovered() ? theme.text.base : theme.text.muted}
wrapMode="none"
selectable={false}
attributes={newTab() ? TextAttributes.BOLD : undefined}
@@ -1184,7 +1184,7 @@ function VerticalSessionTabs(props: {
right={1}
zIndex={2}
width={1}
fg={theme.text.subdued}
fg={theme.text.muted}
selectable={false}
onMouseUp={(event) => {
if (event.button === RIGHT_MOUSE_BUTTON) return
@@ -1217,10 +1217,10 @@ function VerticalSessionTabs(props: {
edge="top"
width={tooltipWidth()}
color={background()}
background={base.background.default}
background={base.background.base}
/>
<box height={2} paddingX={1} backgroundColor={background()}>
<text fg={theme.text.default} wrapMode="none" selectable={false}>
<text fg={theme.text.base} wrapMode="none" selectable={false}>
{Locale.truncateWidth(
data?.session.get(sessionID())?.title ??
items().find((tab) => tab.sessionID === sessionID())?.title ??
@@ -1228,7 +1228,7 @@ function VerticalSessionTabs(props: {
tooltipWidth() - 2,
)}
</text>
<text fg={theme.text.subdued} wrapMode="none" selectable={false}>
<text fg={theme.text.muted} wrapMode="none" selectable={false}>
{Locale.takeWidth(detail(sessionID()), tooltipWidth() - 2)}
</text>
</box>
@@ -1237,7 +1237,7 @@ function VerticalSessionTabs(props: {
edge="bottom"
width={tooltipWidth()}
color={background()}
background={base.background.default}
background={base.background.base}
/>
</box>
)}
@@ -1291,9 +1291,9 @@ function HorizontalSessionTabs(props: {
onCleanup(clearCloseHold)
// A captured drag ends with a synthetic up on its drop target; do not turn that into a click.
let suppressClick = false
const unreadColor = () => theme.text.status.unread
const activeNumber = () => theme.text.status.running
const idleNumber = () => tint(theme.text.formfield.default, theme.background.default, 0.55)
const unreadColor = () => theme.hue.interactive[800]
const activeNumber = () => theme.hue.accent[800]
const idleNumber = () => tint(theme.text.formfield.base, theme.background.base, 0.55)
const newTab = () => tabs.newTab?.() ?? false
const activeID = createMemo(() => (newTab() ? NEW_SESSION_TAB.sessionID : tabs.current()))
const ordered = createMemo(() => {
@@ -1519,7 +1519,7 @@ function HorizontalSessionTabs(props: {
onMouseDragEnd={release}
>
<Show when={layout().before > 0}>
<text width={sessionTabOverflowWidth(layout().before)} fg={theme.text.subdued} selectable={false}>
<text width={sessionTabOverflowWidth(layout().before)} fg={theme.text.muted} selectable={false}>
{layout().before}
</text>
</Show>
@@ -1533,14 +1533,14 @@ function HorizontalSessionTabs(props: {
const dragged = () => dragging() === tab.sessionID
const background = createMemo(() => {
const lifted = (hovered() === tab.sessionID || dragged()) && !selected()
const base = lifted ? theme.background.action.primary.hovered : theme.background.default
const base = lifted ? theme.background.action.primary.hovered : theme.background.base
// A dragged tab lifts to full selected elevation while it is held.
return tint(base, theme.decrease(theme.background.raised.base), dragged() ? 1 : selection())
})
const pulseColor = () => tint(background(), theme.text.default, 0.45)
const pulseColor = () => tint(background(), theme.text.base, 0.45)
// The edge flash washes toward a brighter stop on the same background-to-text ramp,
// so it reads as a lift of the pulse color rather than a different hue.
const flashColor = () => tint(background(), theme.text.default, 0.65)
const flashColor = () => tint(background(), theme.text.base, 0.65)
const feedbackColor = () => tabFeedbackColor(status(), theme)
const glowLevel = createGlowLevel(() => selected() && Boolean(status().attention), animations)
const glowColor = createMemo(() => tint(background(), feedbackColor() ?? unreadColor(), glowLevel()))
@@ -1573,8 +1573,8 @@ function HorizontalSessionTabs(props: {
const runs = () => status().busy && !status().attention
const numberIgnition = createNumberIgnition(runs, () => status().promptPulse, animations)
const foreground = () => {
if (hovered() === tab.sessionID) return theme.text.default
return tint(theme.text.subdued, theme.text.default, selection())
if (hovered() === tab.sessionID) return theme.text.base
return tint(theme.text.muted, theme.text.base, selection())
}
// Title characters sitting over the glow tinge toward its color, following the same
// spatial falloff as the glow itself; characters beyond the tail stay neutral.
@@ -1600,13 +1600,13 @@ function HorizontalSessionTabs(props: {
const base =
hovered() === tab.sessionID && !selected()
? foreground()
: tint(idleNumber(), tint(theme.text.default, background(), 0.25), selection())
: tint(idleNumber(), tint(theme.text.base, background(), 0.25), selection())
const color = runs() ? activeNumber() : (feedback ?? tint(base, unreadColor(), activity()))
// The number brightens faintly as the running sweep passes beneath it.
return tint(color, theme.text.default, Math.max(numberIgnition.value().level, 0.15 * sweepLevel()))
return tint(color, theme.text.base, Math.max(numberIgnition.value().level, 0.15 * sweepLevel()))
}
const bold = () => (selected() || dragged() ? TextAttributes.BOLD : undefined)
const closeColor = () => tint(theme.text.subdued, theme.text.default, 0.6)
const closeColor = () => tint(theme.text.muted, theme.text.base, 0.6)
return (
<box
width={width()}
@@ -1666,7 +1666,7 @@ function HorizontalSessionTabs(props: {
color={numberColor()}
unreadColor={feedbackColor() ?? unreadColor()}
backgroundColor={background()}
flashColor={theme.text.default}
flashColor={theme.text.base}
animations={animations()}
numbers={props.numbers}
spinner={props.spinner}
@@ -1697,7 +1697,7 @@ function HorizontalSessionTabs(props: {
right={1}
zIndex={2}
width={1}
fg={closeHovered() ? theme.text.default : closeColor()}
fg={closeHovered() ? theme.text.base : closeColor()}
selectable={false}
onMouseOver={() => setCloseHovered(true)}
onMouseOut={() => setCloseHovered(false)}
@@ -1725,14 +1725,14 @@ function HorizontalSessionTabs(props: {
}}
</For>
<Show when={layout().after > 0}>
<text width={sessionTabOverflowWidth(layout().after)} fg={theme.text.subdued} selectable={false}>
<text width={sessionTabOverflowWidth(layout().after)} fg={theme.text.muted} selectable={false}>
{" " + layout().after}
</text>
</Show>
<Show when={showPlus()}>
<text
width={ADD_TAB_WIDTH}
fg={addHovered() ? theme.text.default : theme.text.subdued}
fg={addHovered() ? theme.text.base : theme.text.muted}
bg={addHovered() ? theme.background.action.primary.hovered : undefined}
selectable={false}
onMouseOver={() => setAddHovered(true)}
+1 -1
View File
@@ -14,7 +14,7 @@ registerOpencodeSpinner()
export function Spinner(props: { children?: JSX.Element; color?: RGBA; shimmer?: RGBA }) {
const theme = useTheme()
const config = useConfig().data
const color = () => props.color ?? theme.text.subdued
const color = () => props.color ?? theme.text.muted
const [frame, setFrame] = createSignal(0)
createEffect(() => {
if (!(config.animations ?? true) || !props.shimmer) return
@@ -55,7 +55,7 @@ export function StartupLoading(props: { ready: () => boolean }) {
<Show when={show()}>
<box position="absolute" zIndex={5000} left={0} right={0} bottom={1} justifyContent="center" alignItems="center">
<box backgroundColor={theme.background.raised.base} paddingLeft={1} paddingRight={1}>
<Spinner color={theme.text.subdued}>{text()}</Spinner>
<Spinner color={theme.text.muted}>{text()}</Spinner>
</box>
</box>
</Show>
+11 -11
View File
@@ -292,7 +292,7 @@ export function TerminalPane(props: {
// TODO: Revisit when embedded terminal mouse handlers can compose without replacing its internal focus handler.
onMouseDown={() => interact()}
>
<Show when={!failure()} fallback={<text fg={theme.text.feedback.error.default}>{failure()}</text>}>
<Show when={!failure()} fallback={<text fg={theme.text.feedback.error.base}>{failure()}</text>}>
<>
<embeddedTerminal
ref={(value) => {
@@ -340,17 +340,17 @@ function terminalPalette(theme: ResolvedThemeTokens, background: RGBA) {
const bright = 100
const colors = [
background,
theme.text.feedback.error.default,
theme.text.feedback.success.default,
theme.text.feedback.warning.default,
theme.text.feedback.error.base,
theme.text.feedback.success.base,
theme.text.feedback.warning.base,
theme.hue.blue[base],
theme.hue.purple[base],
theme.text.feedback.info.default,
theme.text.default,
theme.text.subdued,
theme.text.feedback.error.subdued,
theme.text.feedback.success.subdued,
theme.text.feedback.warning.subdued,
theme.text.feedback.info.base,
theme.text.base,
theme.text.muted,
theme.text.feedback.error.muted,
theme.text.feedback.success.muted,
theme.text.feedback.warning.muted,
theme.hue.blue[bright],
theme.hue.purple[bright],
theme.hue.cyan[bright],
@@ -359,7 +359,7 @@ function terminalPalette(theme: ResolvedThemeTokens, background: RGBA) {
return Buffer.from(
colors
.map((color, index) => `\x1b]4;${index};${hex(color)}\x1b\\`)
.concat(`\x1b]10;${hex(theme.text.default)}\x1b\\`, `\x1b]11;${hex(background)}\x1b\\`)
.concat(`\x1b]10;${hex(theme.text.base)}\x1b\\`, `\x1b]11;${hex(background)}\x1b\\`)
.join(""),
)
}
+1 -1
View File
@@ -324,7 +324,7 @@ const themeContext = createSimpleContext({
themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`)
const current = createComponentTheme(tokens)
createEffect(() => renderer.setBackgroundColor(tokens().background.default))
createEffect(() => renderer.setBackgroundColor(tokens().background.base))
const currentSyntax = createSyntaxStyleMemo(() => generateSyntax(tokens()))
const service: Themes = {
@@ -22,17 +22,17 @@ function Mcp(props: { context: Plugin.Context }) {
return (
<Show when={list().length}>
<box gap={1} flexDirection="row" flexShrink={0} onMouseUp={() => props.context.keymap.dispatch("mcp.list")}>
<text fg={props.context.theme.text.default}>
<text fg={props.context.theme.text.base}>
<Switch>
<Match when={failed()}>
<span style={{ fg: props.context.theme.text.feedback.error.default }}> </span>
<span style={{ fg: props.context.theme.text.feedback.error.base }}> </span>
{failed()} MCP failed
</Match>
<Match when={true}>
<span
style={{
fg:
count() > 0 ? props.context.theme.text.feedback.success.default : props.context.theme.text.subdued,
count() > 0 ? props.context.theme.text.feedback.success.base : props.context.theme.text.muted,
}}
>
{" "}
@@ -42,7 +42,7 @@ function Mcp(props: { context: Plugin.Context }) {
</Switch>
</text>
<Show when={visibility().mcpCommand}>
<text fg={props.context.theme.text.subdued}>/mcps</text>
<text fg={props.context.theme.text.muted}>/mcps</text>
</Show>
</box>
</Show>
@@ -62,12 +62,12 @@ function Plugins(props: { context: Plugin.Context }) {
return (
<Show when={failed()}>
<box gap={1} flexDirection="row" flexShrink={0} onMouseUp={() => props.context.keymap.dispatch("plugins.list")}>
<text fg={props.context.theme.text.default}>
<span style={{ fg: props.context.theme.text.feedback.error.default }}> </span>
<text fg={props.context.theme.text.base}>
<span style={{ fg: props.context.theme.text.feedback.error.base }}> </span>
{failed()} plugin{failed() === 1 ? "" : "s"} failed
</text>
<Show when={visibility().pluginCommand}>
<text fg={props.context.theme.text.subdued}>/plugins</text>
<text fg={props.context.theme.text.muted}>/plugins</text>
</Show>
</box>
</Show>
@@ -96,7 +96,7 @@ function View(props: { context: Plugin.Context }) {
<box flexGrow={1} />
<Show when={visibility().version}>
<box flexShrink={0}>
<text fg={props.context.theme.text.subdued}>{props.context.app.version}</text>
<text fg={props.context.theme.text.muted}>{props.context.app.version}</text>
</box>
</Show>
</box>
@@ -32,7 +32,7 @@ export default Plugin.define({
return (
<Show when={pending() > 0}>
<box flexShrink={0}>
<Spinner color={theme.text.status.running}>/btw</Spinner>
<Spinner color={theme.hue.accent[800]}>/btw</Spinner>
</box>
</Show>
)
@@ -126,15 +126,15 @@ export function Answer(props: {
<box gap={1}>
<box paddingLeft={2} paddingRight={2}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
/btw
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingTop={1}>
<text fg={theme.text.subdued} wrapMode="word">
<text fg={theme.text.muted} wrapMode="word">
{props.question}
</text>
</box>
@@ -161,12 +161,12 @@ export function Answer(props: {
</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 }}>
<span style={{ fg: copied() ? theme.text.feedback.success.base : theme.text.base }}>
<b>{copied() ? "✓ copied" : "c"}</b>
</span>
<span style={{ fg: theme.text.subdued }}>{copied() ? "" : " copy"}</span>
<span style={{ fg: theme.text.muted }}>{copied() ? "" : " copy"}</span>
</text>
<text fg={theme.text.subdued}>/ scroll</text>
<text fg={theme.text.muted}>/ scroll</text>
</box>
</box>
)
@@ -72,11 +72,11 @@ export function PromptFooter(props: {
onMouseUp={() => props.context.keymap.dispatch("session.child.first")}
>
<text
fg={liveHovered() ? props.context.theme.text.default : props.context.theme.text.subdued}
fg={liveHovered() ? props.context.theme.text.base : props.context.theme.text.muted}
wrapMode="none"
>
<Show when={shortcut("session.child.first")}>
{(value) => <span style={{ fg: props.context.theme.text.default }}>{value()} </span>}
{(value) => <span style={{ fg: props.context.theme.text.base }}>{value()} </span>}
</Show>
<Show when={subagents()}>{(value) => <>{value()}</>}</Show>
<Show when={subagents() && shells()}> · </Show>
@@ -85,7 +85,7 @@ export function PromptFooter(props: {
</box>
</Show>
<Show when={props.showDetails && layout().usage && status().length > 0}>
<text fg={props.context.theme.text.subdued} wrapMode="none" flexShrink={0}>
<text fg={props.context.theme.text.muted} wrapMode="none" flexShrink={0}>
<Show when={live()}> · </Show>
{status().join(" · ")}
</text>
@@ -93,21 +93,21 @@ export function PromptFooter(props: {
</box>
</Match>
<Match when={props.showDetails && layout().shortcuts}>
<text fg={props.context.theme.text.default} flexShrink={0}>
{shortcut("agent.cycle")} <span style={{ fg: props.context.theme.text.subdued }}>agents</span>
<text fg={props.context.theme.text.base} flexShrink={0}>
{shortcut("agent.cycle")} <span style={{ fg: props.context.theme.text.muted }}>agents</span>
</text>
</Match>
</Switch>
<Show when={props.showDetails && layout().shortcuts}>
<text fg={props.context.theme.text.default} wrapMode="none" flexShrink={0}>
{shortcut("command.palette.show")} <span style={{ fg: props.context.theme.text.subdued }}>commands</span>
<text fg={props.context.theme.text.base} wrapMode="none" flexShrink={0}>
{shortcut("command.palette.show")} <span style={{ fg: props.context.theme.text.muted }}>commands</span>
</text>
</Show>
</Match>
<Match when={props.mode === "shell"}>
<text fg={props.context.theme.text.default} flexShrink={0}>
<text fg={props.context.theme.text.base} flexShrink={0}>
esc{" "}
<span style={{ fg: props.context.theme.text.subdued }}>
<span style={{ fg: props.context.theme.text.muted }}>
{dimensions().width < 44 ? "shell" : "exit shell mode"}
</span>
</text>
@@ -20,21 +20,21 @@ export function SidebarContext(props: { context: Plugin.Context; sessionID: stri
return (
<Show when={state() || cost() > 0}>
<box>
<text fg={theme.text.default}>
<text fg={theme.text.base}>
<b>Context</b>
</text>
<Show when={state()}>
{(value) => (
<>
<text fg={theme.text.subdued}>{value().tokens.toLocaleString()} tokens</text>
<text fg={theme.text.muted}>{value().tokens.toLocaleString()} tokens</text>
<Show when={value().percent !== undefined}>
<text fg={theme.text.subdued}>{value().percent}% used</text>
<text fg={theme.text.muted}>{value().percent}% used</text>
</Show>
</>
)}
</Show>
<Show when={cost() > 0}>
<text fg={theme.text.subdued}>{money.format(cost())} spent</text>
<text fg={theme.text.muted}>{money.format(cost())} spent</text>
</Show>
</box>
</Show>
@@ -31,7 +31,7 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
<FilePath
value={value()}
maxWidth={38}
fg={actions.hovered() ? props.context.theme.text.default : props.context.theme.text.subdued}
fg={actions.hovered() ? props.context.theme.text.base : props.context.theme.text.muted}
/>
</box>
)}
@@ -13,11 +13,11 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
)
const dot = (status: string) => {
if (status === "connected") return theme.text.feedback.success.default
if (status === "failed") return theme.text.feedback.error.default
if (status === "disabled") return theme.text.subdued
if (status === "needs_auth") return theme.text.feedback.warning.default
return theme.text.subdued
if (status === "connected") return theme.text.feedback.success.base
if (status === "failed") return theme.text.feedback.error.base
if (status === "disabled") return theme.text.muted
if (status === "needs_auth") return theme.text.feedback.warning.base
return theme.text.muted
}
return (
@@ -25,12 +25,12 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
<box>
<box flexDirection="row" gap={1} onMouseDown={() => list().length > 2 && setOpen((x) => !x)}>
<Show when={list().length > 2}>
<text fg={theme.text.default}>{open() ? "▼" : "▶"}</text>
<text fg={theme.text.base}>{open() ? "▼" : "▶"}</text>
</Show>
<text fg={theme.text.default}>
<text fg={theme.text.base}>
<b>MCP</b>
<Show when={!open()}>
<span style={{ fg: theme.text.subdued }}>
<span style={{ fg: theme.text.muted }}>
{" "}
({on()} active{bad() > 0 ? `, ${bad()} error${bad() > 1 ? "s" : ""}` : ""})
</span>
@@ -58,11 +58,11 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
>
</text>
<text fg={theme.text.default} wrapMode="none" truncate flexGrow={1} flexShrink={1} minWidth={0}>
<text fg={theme.text.base} wrapMode="none" truncate flexGrow={1} flexShrink={1} minWidth={0}>
<b>{item.name}</b>
</text>
<text
fg={item.status.status === "failed" ? theme.text.feedback.error.default : theme.text.subdued}
fg={item.status.status === "failed" ? theme.text.feedback.error.base : theme.text.muted}
wrapMode="none"
flexShrink={0}
>
@@ -77,7 +77,7 @@ export function DiffFileMenu(props: {
if (event.button === MouseButton.LEFT) run()
}}
>
<text fg={theme.text.default} selectable={false} wrapMode="none" truncate>
<text fg={theme.text.base} selectable={false} wrapMode="none" truncate>
{label()}
</text>
</box>
@@ -36,10 +36,10 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
? flattenFileTree(tree()).filter((row) => row.fileIndex !== undefined)
: flattenFileTree(tree(), props.expandedNodes),
)
// Quieter than subdued text: markers are affordances, not content.
const faint = createMemo(() => tint(theme.text.subdued, theme.background.raised.base, 0.45))
// Quieter than muted text: markers are affordances, not content.
const faint = createMemo(() => tint(theme.text.muted, theme.background.raised.base, 0.45))
// Rails are pure texture; keep them barely above the surface.
const rail = createMemo(() => tint(theme.text.subdued, theme.background.raised.base, 0.7))
const rail = createMemo(() => tint(theme.text.muted, theme.background.raised.base, 0.7))
const reviewedCount = createMemo(() => props.files.filter((file) => props.reviewedFileNames?.has(file.file)).length)
const contentWidth = () => Math.max(0, props.width - 4 - FILE_TREE_STATUS_WIDTH - 1)
let scroll: ScrollBoxRenderable | undefined
@@ -85,8 +85,8 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
props.onSwitchSource
? sourceHovered()
? theme.text.action.secondary.hovered
: theme.text.action.secondary.default
: theme.text.default
: theme.text.action.secondary.base
: theme.text.base
}
attributes={TextAttributes.BOLD}
flexShrink={0}
@@ -96,12 +96,12 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
{props.source ?? "Files"}
</text>
<Show when={props.sourceDetail}>
<text fg={theme.text.subdued} selectable={false} flexGrow={1} minWidth={0} wrapMode="none" truncate>
<text fg={theme.text.muted} selectable={false} flexGrow={1} minWidth={0} wrapMode="none" truncate>
{` · ${props.sourceDetail}`}
</text>
</Show>
</box>
<text id="diff-review-count" fg={theme.text.subdued} wrapMode="none" flexShrink={0}>
<text id="diff-review-count" fg={theme.text.muted} wrapMode="none" flexShrink={0}>
{reviewedCount()}/{props.files.length}
{props.source ? "" : " reviewed"}
</text>
@@ -119,7 +119,7 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
<text />
</Match>
<Match when={props.files.length === 0}>
<text fg={theme.text.subdued}>No files</text>
<text fg={theme.text.muted}>No files</text>
</Match>
<Match when={props.files.length > 0}>
<box flexShrink={0} gap={list() ? 1 : 0}>
@@ -132,8 +132,8 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
return file !== undefined && (props.reviewedFileNames?.has(file) ?? false)
}
const foreground = () => {
if (row.kind === "directory") return theme.text.subdued
return reviewed() ? theme.text.subdued : theme.text.default
if (row.kind === "directory") return theme.text.muted
return reviewed() ? theme.text.muted : theme.text.base
}
const background = () => {
// Elevated context maps this to a quiet neutral surface step, not the loud accent.
@@ -151,11 +151,11 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
})
const status = () => fileTreeRowStatus(row, props.files, reviewed())
const statusColor = () => {
if (reviewed()) return theme.text.subdued
if (reviewed()) return theme.text.muted
const status = row.fileIndex === undefined ? undefined : props.files[row.fileIndex]?.status
if (status === "added") return theme.diff.text.added
if (status === "deleted") return theme.diff.text.removed
return theme.text.subdued
return theme.text.muted
}
const name = () => {
const width = contentWidth() - stringWidth(indent()) - stringWidth(marker())
@@ -30,14 +30,14 @@ export function DiffViewerImage(props: {
return (
<box width="100%" flexShrink={0} gap={1} paddingLeft={1} paddingRight={1} paddingBottom={1}>
<text fg={theme.text.subdued}>{props.label ?? "Working tree preview"}</text>
<text fg={theme.text.muted}>{props.label ?? "Working tree preview"}</text>
<box height={height() + 2} flexShrink={0} gap={1}>
<Switch>
<Match when={image.error}>
<text fg={theme.text.feedback.error.default}>Could not load image</text>
<text fg={theme.text.feedback.error.base}>Could not load image</text>
</Match>
<Match when={image.loading}>
<text fg={theme.text.subdued}>Loading image</text>
<text fg={theme.text.muted}>Loading image</text>
</Match>
<Match when={!image.error && image()} keyed>
{(bytes) => {
@@ -61,7 +61,7 @@ export function DiffViewerImage(props: {
return (
<Show
when={!failed()}
fallback={<text fg={theme.text.feedback.error.default}>Could not decode image</text>}
fallback={<text fg={theme.text.feedback.error.base}>Could not decode image</text>}
>
<box width="100%" height={height()} onMouseUp={open}>
<image
@@ -78,8 +78,8 @@ export function DiffViewerImage(props: {
<Show when={size()}>
{(value) => (
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text.subdued}>{value()}</text>
<text fg={theme.text.action.secondary.default} onMouseUp={open}>
<text fg={theme.text.muted}>{value()}</text>
<text fg={theme.text.action.secondary.base} onMouseUp={open}>
Click to enlarge
</text>
</box>
@@ -221,7 +221,7 @@ function DiffBaseDialog(props: {
)
const Empty = () => (
<box paddingLeft={4} paddingRight={4}>
<text fg={branches.error ? theme.text.feedback.error.default : theme.text.subdued}>
<text fg={branches.error ? theme.text.feedback.error.base : theme.text.muted}>
{branches.loading
? "Loading branches…"
: branches.error
@@ -240,7 +240,7 @@ function DiffBaseDialog(props: {
onFilter={setSearch}
emptyView={<Empty />}
noMatchView={<Empty />}
footer={<text fg={theme.text.subdued}>Remembered until the TUI exits</text>}
footer={<text fg={theme.text.muted}>Remembered until the TUI exits</text>}
options={(branches.loading || branches.error ? [] : (branches()?.data ?? [])).map((name) => ({
title: name,
value: name,
@@ -758,7 +758,7 @@ export function DiffViewerContent(props: {
{(shortcut) => (
<text
id="diff-help-shortcut"
fg={theme.text.default}
fg={theme.text.base}
selectable={false}
flexShrink={0}
wrapMode="none"
@@ -770,7 +770,7 @@ export function DiffViewerContent(props: {
>
{props.compact ? "?" : shortcut()}
<Show when={!props.compact}>
<span style={{ fg: theme.text.subdued }}> help</span>
<span style={{ fg: theme.text.muted }}> help</span>
</Show>
</text>
)}
@@ -782,7 +782,7 @@ export function DiffViewerContent(props: {
}))
return (
<box width="100%" height="100%" backgroundColor={theme.background.default}>
<box width="100%" height="100%" backgroundColor={theme.background.base}>
<Show when={!showFileTree()}>
<box
id="diff-source-header"
@@ -805,7 +805,7 @@ export function DiffViewerContent(props: {
}}
>
<text
fg={theme.text.action.secondary.default}
fg={theme.text.action.secondary.base}
attributes={TextAttributes.BOLD}
selectable={false}
flexShrink={0}
@@ -814,12 +814,12 @@ export function DiffViewerContent(props: {
{diffSourceLabel(mode())}
</text>
<Show when={props.sourceDetail}>
<text fg={theme.text.subdued} selectable={false} flexGrow={1} minWidth={0} wrapMode="none" truncate>
<text fg={theme.text.muted} selectable={false} flexGrow={1} minWidth={0} wrapMode="none" truncate>
{` · ${props.sourceDetail}`}
</text>
</Show>
</box>
<text id="diff-review-count" fg={theme.text.subdued} flexShrink={0} wrapMode="none">
<text id="diff-review-count" fg={theme.text.muted} flexShrink={0} wrapMode="none">
{files().filter((file) => reviewedFileNames().has(file.file)).length}/{files().length}
</text>
</box>
@@ -828,12 +828,12 @@ export function DiffViewerContent(props: {
<Switch>
<Match when={props.loading}>
<box flexGrow={1} padding={2}>
<text fg={theme.text.subdued}>Loading diff</text>
<text fg={theme.text.muted}>Loading diff</text>
</box>
</Match>
<Match when={!props.loading && props.error}>
<box flexGrow={1} padding={2}>
<text fg={theme.text.feedback.error.default}>
<text fg={theme.text.feedback.error.base}>
{!props.sourceBase && mode() !== "working"
? "Could not load diff. Choose a base branch from Diff source, or select Uncommitted."
: "Could not load diff. Reopen the diff viewer to try again."}
@@ -842,14 +842,14 @@ export function DiffViewerContent(props: {
</Match>
<Match when={!props.loading && props.unavailable}>
<box flexGrow={1} padding={2}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
Committed comparison unavailable without base metadata. Choose a base branch from Diff source.
</text>
</box>
</Match>
<Match when={!props.loading && files().length === 0}>
<box flexGrow={1} padding={2}>
<text fg={theme.text.subdued}>No changes to show</text>
<text fg={theme.text.muted}>No changes to show</text>
</box>
</Match>
<Match when={!props.loading}>
@@ -924,7 +924,7 @@ export function DiffViewerContent(props: {
flexShrink={0}
border={["top"]}
borderColor={background()}
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
customBorderChars={{ ...EmptyBorder, horizontal: "▄" }}
/>
</Show>
@@ -961,20 +961,20 @@ export function DiffViewerContent(props: {
<FilePath
value={entry.file.file}
maxWidth={Math.max(1, patchPaneWidth() - countsWidth() - 2)}
fg={theme.text.subdued}
basenameFg={reviewed() ? theme.text.subdued : theme.text.default}
fg={theme.text.muted}
basenameFg={reviewed() ? theme.text.muted : theme.text.base}
/>
</box>
<Show when={reviewed()}>
<text fg={theme.text.subdued} flexShrink={0}>
<text fg={theme.text.muted} flexShrink={0}>
</text>
</Show>
<Show when={!image()} fallback={<text fg={theme.text.subdued}>Image</text>}>
<text flexShrink={0} fg={reviewed() ? theme.text.subdued : theme.diff.text.added}>
<Show when={!image()} fallback={<text fg={theme.text.muted}>Image</text>}>
<text flexShrink={0} fg={reviewed() ? theme.text.muted : theme.diff.text.added}>
+{entry.file.additions}
</text>
<text flexShrink={0} fg={reviewed() ? theme.text.subdued : theme.diff.text.removed}>
<text flexShrink={0} fg={reviewed() ? theme.text.muted : theme.diff.text.removed}>
-{entry.file.deletions}
</text>
</Show>
@@ -987,7 +987,7 @@ export function DiffViewerContent(props: {
height={1}
border={["bottom"]}
borderColor={background()}
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
customBorderChars={{ ...EmptyBorder, horizontal: "▀" }}
/>
</Show>
@@ -996,7 +996,7 @@ export function DiffViewerContent(props: {
<Switch
fallback={
<box width="100%" flexShrink={0} paddingLeft={1} paddingRight={1} paddingBottom={1}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{mode() === "committed" && image()
? "Committed image preview unavailable. The working-tree image is not shown."
: entry.file.status === "deleted" && image()
@@ -1031,7 +1031,7 @@ export function DiffViewerContent(props: {
showLineNumbers={true}
width="100%"
wrapMode="char"
fg={theme.text.default}
fg={theme.text.base}
addedBg={theme.diff.background.added}
removedBg={theme.diff.background.removed}
contextBg={theme.diff.background.context}
@@ -1125,10 +1125,10 @@ function DiffViewerHelpDialog(props: { context: Plugin.Context; single: boolean
return (
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
Diff shortcuts
</text>
<text fg={theme.text.subdued} selectable={false} onMouseUp={() => props.context.ui.dialog.clear()}>
<text fg={theme.text.muted} selectable={false} onMouseUp={() => props.context.ui.dialog.clear()}>
esc close
</text>
</box>
@@ -1149,16 +1149,16 @@ function DiffViewerHelpDialog(props: { context: Plugin.Context; single: boolean
<For each={groups}>
{(group) => (
<box flexShrink={0}>
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
{group.title}
</text>
<For each={group.rows}>
{(row) => (
<box flexDirection="row" gap={2}>
<text fg={theme.text.default} width={17} flexShrink={0}>
<text fg={theme.text.base} width={17} flexShrink={0}>
{row.shortcut() || "unbound"}
</text>
<text fg={theme.text.subdued} flexGrow={1} minWidth={0}>
<text fg={theme.text.muted} flexGrow={1} minWidth={0}>
{row.label}
</text>
</box>
@@ -98,14 +98,14 @@ export function PluginsDialog(props: {
footer: updating(entry) ? "updating" : footer(entry),
footerColor:
status(entry) === "failed"
? props.context.theme.text.feedback.error.default
? props.context.theme.text.feedback.error.base
: outdated(entry)
? props.context.theme.text.feedback.info.default
: props.context.theme.text.subdued,
? props.context.theme.text.feedback.info.base
: props.context.theme.text.muted,
gutter: updating(entry)
? (color) => <Spinner color={color} />
: status(entry) === "failed"
? () => <text fg={props.context.theme.text.feedback.error.default}>x</text>
? () => <text fg={props.context.theme.text.feedback.error.base}>x</text>
: undefined,
}),
),
@@ -249,10 +249,10 @@ export function PluginsDialog(props: {
footer={
<Show when={pluginError(focusedEntry()) && !focusedTui()}>
<text>
<span style={{ fg: props.context.theme.text.default }}>
<span style={{ fg: props.context.theme.text.base }}>
<b>enter</b>
</span>
<span style={{ fg: props.context.theme.text.subdued }}> view error</span>
<span style={{ fg: props.context.theme.text.muted }}> view error</span>
</text>
</Show>
}
@@ -52,19 +52,19 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) {
}),
)
const shades = createMemo(() => [
theme.text.subdued,
theme.text.muted,
...[0.3, 0.5, 0.75, 1].map((alpha) =>
tint(theme.background.default, theme.categorical[0][200], alpha),
tint(theme.background.base, theme.categorical[0][200], alpha),
),
])
return (
<box width={width()} flexDirection="column" alignItems="center" flexShrink={0} gap={compact() ? 1 : 2}>
<box width="100%" flexDirection={width() < 44 ? "column" : "row"} justifyContent="space-between">
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
opencode / stats
</text>
<text fg={theme.text.subdued}>{dates()}</text>
<text fg={theme.text.muted}>{dates()}</text>
</box>
<Show when={!compact()}>
<Logo />
@@ -73,7 +73,7 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) {
<Show
when={large()}
fallback={
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
{number()}
</text>
}
@@ -81,7 +81,7 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) {
<box>
<For each={[0, 1, 2, 3, 4]}>
{(row) => (
<text fg={theme.text.default} selectable={false}>
<text fg={theme.text.base} selectable={false}>
{letters()
.map((char) => char[row].replaceAll("1", "\u2588\u2588").replaceAll("0", " "))
.join(" ")}
@@ -90,10 +90,10 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) {
</For>
</box>
</Show>
<text fg={theme.text.subdued}>TOKENS</text>
<text fg={theme.text.muted}>TOKENS</text>
</box>
<box alignItems="center">
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{" " +
calendar()
.months.map((month) => (month.label.length <= month.span * 2 ? month.label : "").padEnd(month.span * 2))
@@ -102,7 +102,7 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) {
<For each={["M", "T", "W", "T", "F", "S", "S"]}>
{(day, index) => (
<box flexDirection="row" height={1}>
<text fg={theme.text.subdued}>{day + " "}</text>
<text fg={theme.text.muted}>{day + " "}</text>
<For each={calendar().weeks}>
{(week) => (
<text fg={shades()[Math.max(0, week[index()].level)]} selectable={false}>
@@ -114,24 +114,24 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) {
)}
</For>
<Show when={calendar().clipped}>
<text fg={theme.text.subdued}>Your last {calendar().weeks.length} weeks</text>
<text fg={theme.text.muted}>Your last {calendar().weeks.length} weeks</text>
</Show>
</box>
<box width="100%" flexDirection="row" justifyContent="space-around">
<For each={metrics().slice(1)}>
{(metric) => (
<box alignItems="center">
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
{statsNumber(metric.value)}
{metric.label === "best streak" ? " days" : ""}
</text>
<text fg={theme.text.subdued}>{metric.label}</text>
<text fg={theme.text.muted}>{metric.label}</text>
</box>
)}
</For>
</box>
<box width="100%" flexDirection="row" justifyContent="flex-end">
<text fg={theme.text.default}>opencode.ai</text>
<text fg={theme.text.base}>opencode.ai</text>
</box>
</box>
)
@@ -153,7 +153,7 @@ function StatsPage(props: { context: Plugin.Context; onClose: () => void }) {
}))
return (
<box width="100%" height="100%" backgroundColor={theme.background.default}>
<box width="100%" height="100%" backgroundColor={theme.background.base}>
<scrollbox
flexGrow={1}
contentOptions={{
@@ -167,10 +167,10 @@ function StatsPage(props: { context: Plugin.Context; onClose: () => void }) {
<Show
when={!result.error}
fallback={
<text fg={theme.text.feedback.error.default}>Could not load stats. Reopen /stats to try again.</text>
<text fg={theme.text.feedback.error.base}>Could not load stats. Reopen /stats to try again.</text>
}
>
<Show when={result()} fallback={<text fg={theme.text.subdued}>Gathering your stats</text>}>
<Show when={result()} fallback={<text fg={theme.text.muted}>Gathering your stats</text>}>
{(value) => <StatsPoster stats={value()} />}
</Show>
</Show>
@@ -19,16 +19,16 @@ export function StoryFooter(props: {
return (
<box flexShrink={0} flexDirection="column" backgroundColor={theme.background.raised.base}>
<box height={1} paddingLeft={1} paddingRight={1} flexDirection="row">
<text fg={theme.text.default}>{props.title}</text>
<text fg={theme.text.base}>{props.title}</text>
<Show when={props.details?.length}>
<text fg={theme.text.subdued}> · {props.details?.join(" · ")}</text>
<text fg={theme.text.muted}> · {props.details?.join(" · ")}</text>
</Show>
</box>
<Show when={props.status || props.message}>
<box height={1} paddingLeft={1} paddingRight={1} flexDirection="row">
<text fg={theme.text.default} wrapMode="none">
<text fg={theme.text.base} wrapMode="none">
{props.status ?? ""}
<span style={{ fg: theme.text.subdued }}>
<span style={{ fg: theme.text.muted }}>
{props.status && props.message ? " · " : ""}
{props.message ?? ""}
</span>
@@ -38,8 +38,8 @@ export function StoryFooter(props: {
<box paddingLeft={1} paddingRight={1} flexDirection="row" flexWrap="wrap" columnGap={1}>
<For each={props.controls}>
{(control) => (
<text fg={theme.text.default} wrapMode="none" flexShrink={0}>
{control.shortcut} <span style={{ fg: theme.text.subdued }}>{control.label}</span>
<text fg={theme.text.base} wrapMode="none" flexShrink={0}>
{control.shortcut} <span style={{ fg: theme.text.muted }}>{control.label}</span>
</text>
)}
</For>
@@ -97,15 +97,15 @@ function StorybookIndex(props: { context: Plugin.Context }) {
width={dimensions().width}
height={dimensions().height}
flexDirection="column"
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
>
<box paddingTop={2} paddingLeft={2} flexDirection="column">
<text fg={theme.text.default}>storybook</text>
<text fg={theme.text.subdued}>fixture-driven simulations of production components</text>
<text fg={theme.text.base}>storybook</text>
<text fg={theme.text.muted}>fixture-driven simulations of production components</text>
<box height={1} />
<For each={stories}>
{(story, index) => (
<text fg={index() === selected() ? theme.text.default : theme.text.subdued}>
<text fg={index() === selected() ? theme.text.base : theme.text.muted}>
{index() === selected() ? " " : " "}
{index() + 1} {story.title}
</text>
@@ -156,14 +156,14 @@ function MermanLayoutsStory(props: { context: Plugin.Context }) {
width={dimensions().width}
height={dimensions().height}
flexDirection="column"
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
>
<Show when={rendered()} keyed>
{(item) => (
<scrollbox flexGrow={1} minHeight={0} viewportOptions={{ paddingRight: 1 }}>
<box paddingLeft={2} paddingRight={2} paddingTop={1} flexDirection="column">
<text fg={theme.text.default}>{item.fixture.title}</text>
<text fg={theme.text.subdued}>{item.fixture.id}</text>
<text fg={theme.text.base}>{item.fixture.title}</text>
<text fg={theme.text.muted}>{item.fixture.id}</text>
<box height={1} />
<markdown
width="100%"
@@ -173,7 +173,7 @@ function MermanLayoutsStory(props: { context: Plugin.Context }) {
tableOptions={{ style: "grid", cellPaddingX: 1 }}
conceal={true}
fg={theme.markdown.text}
bg={theme.background.default}
bg={theme.background.base}
renderNode={plugins.markdown()}
/>
</box>
@@ -127,12 +127,12 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
<box
width={dimensions().width}
height={dimensions().height}
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
justifyContent={solo() ? "center" : undefined}
alignItems={solo() ? "center" : undefined}
>
<Show when={!solo()}>
<text fg={theme.text.default} flexShrink={0}>
<text fg={theme.text.base} flexShrink={0}>
one-cell motion lab.
</text>
</Show>
@@ -141,17 +141,17 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
<>
<Show when={!solo()}>
<box flexDirection="row" height={1} flexShrink={0} paddingLeft={1}>
<text width={22} fg={theme.text.subdued}>
<text width={22} fg={theme.text.muted}>
pattern
</text>
<For each={speeds()}>
{(value) => (
<text width={7} fg={theme.text.subdued}>
<text width={7} fg={theme.text.muted}>
{value}x
</text>
)}
</For>
<text fg={theme.text.subdued}>cycle @1x</text>
<text fg={theme.text.muted}>cycle @1x</text>
</box>
<scrollbox
ref={scroll}
@@ -166,7 +166,7 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
<text
width={22}
wrapMode="none"
fg={index() === selected() ? theme.text.formfield.selected : theme.text.formfield.default}
fg={index() === selected() ? theme.text.formfield.selected : theme.text.formfield.base}
>
{index() === selected() ? ">" : " "}
{String(index() + 1).padStart(2)} {item.name.toLowerCase()}.
@@ -181,16 +181,16 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
animations={animations()}
paused={paused()}
glow={glow()}
color={theme.text.status.running}
color={theme.hue.accent[800]}
/>
</box>
)}
</For>
<text width={10} fg={theme.text.subdued}>
<text width={10} fg={theme.text.muted}>
{item.pace ? "adaptive" : `${item.frames.length * item.interval}ms`}
</text>
<Show when={dimensions().width >= 80}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{[...new Set(item.frames)].join(" ")}
{item.levels ? " + intensity" : ""}
</text>
@@ -207,15 +207,15 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
paddingRight={1}
alignItems={solo() ? "center" : undefined}
>
<text fg={theme.text.default} maxWidth="100%" attributes={solo() ? TextAttributes.BOLD : 0}>
<text fg={theme.text.base} maxWidth="100%" attributes={solo() ? TextAttributes.BOLD : 0}>
<Show when={!solo()}>{String(selected() + 1).padStart(2, "0")} / </Show>
{animation().name.toLowerCase()}.
</text>
<Show when={!solo()}>
<text fg={theme.text.subdued} maxWidth="100%">
<text fg={theme.text.muted} maxWidth="100%">
{animation().description}
</text>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{speed()}x: {timing()}
</text>
</Show>
@@ -226,7 +226,7 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
marginTop={solo() ? 1 : 0}
>
<box height={1} flexDirection="row">
<text width={7} fg={theme.text.subdued}>
<text width={7} fg={theme.text.muted}>
work
</text>
<OneCellSpinner
@@ -236,12 +236,12 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
animations={animations()}
paused={paused()}
glow={glow()}
color={theme.text.status.running}
color={theme.hue.accent[800]}
/>
<text fg={theme.text.default}> esc stop</text>
<text fg={theme.text.base}> esc stop</text>
</box>
<box height={1} flexDirection="row">
<text width={7} fg={theme.text.subdued}>
<text width={7} fg={theme.text.muted}>
launch
</text>
<OneCellSpinner
@@ -250,11 +250,11 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
animations={animations()}
paused={paused()}
glow={glow()}
color={theme.text.default}
color={theme.text.base}
/>
<text fg={theme.text.default} wrapMode="none">
<text fg={theme.text.base} wrapMode="none">
{splash().label.slice(1)}
<span style={{ fg: theme.text.subdued }}>{splash().metadata}</span>
<span style={{ fg: theme.text.muted }}>{splash().metadata}</span>
</text>
</box>
</box>
@@ -47,16 +47,16 @@ function SessionLocationMissingStory(props: { context: Plugin.Context }) {
return (
<box width={dimensions().width} height={dimensions().height} backgroundColor={theme.background.raised.base}>
<box paddingLeft={2} paddingRight={2} paddingTop={1} flexGrow={1}>
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
Workerd Modal workspace driver
</text>
<text fg={theme.text.subdued}>build · GPT-5.6 Sol (high)</text>
<text fg={theme.text.muted}>build · GPT-5.6 Sol (high)</text>
<box height={1} />
<text fg={theme.text.default}>You</text>
<text fg={theme.text.subdued}>Test the mounted workspace and verify the deployment.</text>
<text fg={theme.text.base}>You</text>
<text fg={theme.text.muted}>Test the mounted workspace and verify the deployment.</text>
<box height={1} />
<text fg={theme.text.default}>Build · GPT-5.6 Sol (high)</text>
<text fg={theme.text.subdued}>The deployment is verified and the worktree is clean.</text>
<text fg={theme.text.base}>Build · GPT-5.6 Sol (high)</text>
<text fg={theme.text.muted}>The deployment is verified and the worktree is clean.</text>
<box flexGrow={1} />
<SessionLocationUnavailable directory={directory} onMove={open} />
</box>
@@ -258,7 +258,7 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
// sessions; the tail line tracks the live status of the current run.
const transcript = () => {
const current = active()
if (!current) return [{ text: "no session selected", color: theme.text.subdued }]
if (!current) return [{ text: "no session selected", color: theme.text.muted }]
const index = Math.max(
0,
FIXTURE_TABS.findIndex((fixture) => fixture.sessionID === current),
@@ -268,35 +268,35 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
const outcome = outcomes()[current]
const file = TRANSCRIPT_FILES[index % TRANSCRIPT_FILES.length]
const lines = [
{ text: `> ${fixture.title}`, color: theme.text.default },
{ text: "", color: theme.text.default },
{ text: `> ${fixture.title}`, color: theme.text.base },
{ text: "", color: theme.text.base },
]
if (!status.busy && !status.attention && outcome === undefined) {
lines.push({ text: "no activity yet — press s to run this session", color: theme.text.subdued })
lines.push({ text: "no activity yet — press s to run this session", color: theme.text.muted })
return lines
}
lines.push(
{ text: "● Taking a look — reading the relevant code first.", color: theme.text.default },
{ text: "", color: theme.text.default },
{ text: ` ✱ Read ${file}`, color: theme.text.subdued },
{ text: ` ✱ Edit ${file}`, color: theme.text.subdued },
{ text: ` ✱ Bash bun run test`, color: theme.text.subdued },
{ text: "", color: theme.text.default },
{ text: "● Taking a look — reading the relevant code first.", color: theme.text.base },
{ text: "", color: theme.text.base },
{ text: ` ✱ Read ${file}`, color: theme.text.muted },
{ text: ` ✱ Edit ${file}`, color: theme.text.muted },
{ text: ` ✱ Bash bun run test`, color: theme.text.muted },
{ text: "", color: theme.text.base },
)
if (status.attention === "question")
lines.push({ text: "? Which approach should I take?", color: theme.text.status.question })
lines.push({ text: "? Which approach should I take?", color: theme.hue.interactive[800] })
else if (status.attention === "permission")
lines.push({ text: "! Waiting for permission to run the command", color: theme.text.status.permission })
else if (status.busy) lines.push({ text: "● Working…", color: theme.text.status.running })
lines.push({ text: "! Waiting for permission to run the command", color: theme.hue.interactive[800] })
else if (status.busy) lines.push({ text: "● Working…", color: theme.hue.accent[800] })
else if (outcome === "failed")
lines.push({
text: `✗ bun run test failed — 3 tests failing in ${file}`,
color: theme.text.feedback.error.default,
color: theme.text.feedback.error.base,
})
else
lines.push({
text: `✓ Done — updated ${file} and the tests pass.`,
color: theme.text.feedback.success.default,
color: theme.text.feedback.success.base,
})
return lines
}
@@ -493,7 +493,7 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
width={dimensions().width}
height={dimensions().height}
flexDirection="column"
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
>
<box
flexGrow={1}
+30 -30
View File
@@ -1,6 +1,6 @@
import { RGBA, SyntaxStyle, type CliRenderer, type ColorInput, type TerminalColors } from "@opentui/core"
import { generateSyntax, resolveThemeDocument, themeModes, type ResolvedTheme } from "@opencode/theme/tui"
import { allThemes, DEFAULT_THEMES, isThemeSource, parseTheme, type ThemeDocumentSource } from "../theme"
import { allThemes, getOpenCodeTheme, isThemeSource, parseTheme, type ThemeDocumentSource } from "../theme"
import { ansiToRgba } from "../theme/color"
import { discoverThemes } from "../theme/discovery"
import { generateSystem, terminalMode } from "../theme/system"
@@ -116,8 +116,8 @@ function map(
): RunTheme {
// 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)
if (system && color.equals(theme.background.default)) return RGBA.defaultBackground(color)
if (system && color.equals(theme.text.base)) return RGBA.defaultForeground(color)
if (system && color.equals(theme.background.base)) return RGBA.defaultBackground(color)
return color
}
const scrollback = (color: RGBA) => {
@@ -133,50 +133,50 @@ function map(
})
return {
background: RGBA.defaultBackground(theme.background.default),
background: RGBA.defaultBackground(theme.background.base),
footer: {
actionSecondaryText: exact(theme.text.action.secondary.default),
actionSecondaryText: exact(theme.text.action.secondary.base),
actionFocusedBg: exact(theme.background.action.primary.focused),
actionFocusedText: exact(theme.text.action.primary.focused),
formfieldText: exact(theme.text.formfield.default),
formfieldText: exact(theme.text.formfield.base),
formfieldFocusedBg: exact(theme.background.formfield.focused),
formfieldFocusedText: exact(theme.text.formfield.focused),
selection: exact(theme.text.formfield.selected),
running: exact(theme.text.status.running),
question: exact(theme.text.status.question),
permission: exact(theme.text.status.permission),
success: exact(theme.text.feedback.success.default),
running: exact(theme.hue.accent[800]),
question: exact(theme.hue.interactive[800]),
permission: exact(theme.hue.interactive[800]),
success: exact(theme.text.feedback.success.base),
link: exact(theme.markdown.link),
categorical: dedupeWith(
theme.categorical.map((scale) => exact(scale[200])),
(a, b) => a.equals(b),
),
warning: exact(theme.text.feedback.warning.default),
error: exact(theme.text.feedback.error.default),
muted: exact(theme.text.subdued),
text: exact(theme.text.default),
warning: exact(theme.text.feedback.warning.base),
error: exact(theme.text.feedback.error.base),
muted: exact(theme.text.muted),
text: exact(theme.text.base),
shade: exact(theme.background.raised.base),
surface: exact(theme.background.raised.base),
pane: exact(theme.background.raised.high),
border: exact(theme.border.default),
border: exact(theme.border.base),
line: exact(theme.background.raised.high),
},
entry: {
system: { body: scrollback(theme.text.subdued) },
user: { body: scrollback(theme.text.default) },
system: { body: scrollback(theme.text.muted) },
user: { body: scrollback(theme.text.base) },
assistant: { body: scrollback(theme.markdown.text) },
reasoning: { body: scrollback(theme.text.subdued) },
tool: { body: scrollback(theme.text.subdued), start: scrollback(theme.text.subdued) },
error: { body: scrollback(theme.text.feedback.error.default) },
reasoning: { body: scrollback(theme.text.muted) },
tool: { body: scrollback(theme.text.muted), start: scrollback(theme.text.muted) },
error: { body: scrollback(theme.text.feedback.error.base) },
},
splash: {
left: nearestIndexed(indexed, theme.text.subdued),
right: nearestIndexed(indexed, theme.text.default),
left: nearestIndexed(indexed, theme.text.muted),
right: nearestIndexed(indexed, theme.text.base),
leftShadow: nearestIndexed(indexed, theme.background.raised.base),
},
block: {
text: scrollback(theme.text.default),
muted: scrollback(theme.text.subdued),
text: scrollback(theme.text.base),
muted: scrollback(theme.text.muted),
syntax,
diffRemoved: scrollback(theme.diff.text.removed),
diffAddedBg: scrollback(theme.diff.background.added),
@@ -192,11 +192,11 @@ function map(
}
export const RUN_THEME_FALLBACK = map(
resolveThemeDocument(parseTheme(DEFAULT_THEMES.opencode), "dark"),
resolveThemeDocument(parseTheme(getOpenCodeTheme()), "dark"),
ansiPalette,
)
export const RUN_THEME_FALLBACK_LIGHT = map(
resolveThemeDocument(parseTheme(DEFAULT_THEMES.opencode), "light"),
resolveThemeDocument(parseTheme(getOpenCodeTheme()), "light"),
ansiPalette,
)
@@ -292,13 +292,13 @@ export async function resolveRunTheme(
if (themeModes(document).includes(mode)) return resolveThemeDocument(document, mode)
})
.catch(() => undefined)
const theme = resolved ?? resolveThemeDocument(parseTheme(DEFAULT_THEMES.opencode), mode)
const theme = resolved ?? resolveThemeDocument(parseTheme(getOpenCodeTheme()), mode)
const indexed = colors
? ansiPalette.map((color, index) => (colors.palette[index] ? RGBA.fromIndex(index, colors.palette[index]!) : color))
: ansiPalette
return {
...map(theme, indexed, generateSyntax(theme), name === "system" && resolved !== undefined),
background: RGBA.defaultBackground(colors?.defaultBackground ?? theme.background.default),
background: RGBA.defaultBackground(colors?.defaultBackground ?? theme.background.base),
}
}
@@ -312,6 +312,6 @@ async function themeSource(
const custom = await discoverThemes(
configDirectories(process.env.OPENCODE_CONFIG_DIR ?? Global.Path.config, process.cwd()),
)
const source = custom[name] ?? allThemes()[name] ?? DEFAULT_THEMES.opencode
return isThemeSource(source) ? source : DEFAULT_THEMES.opencode
const source = custom[name] ?? allThemes()[name] ?? getOpenCodeTheme()
return isThemeSource(source) ? source : getOpenCodeTheme()
}
+2 -2
View File
@@ -126,7 +126,7 @@ function UpdateNotification(props: { width: number }) {
const exit = useExit()
const theme = useTheme()
const [hovered, setHovered] = createSignal(false)
const backdrop = () => (hovered() ? theme.background.action.primary.hovered : theme.background.default)
const backdrop = () => (hovered() ? theme.background.action.primary.hovered : theme.background.base)
createEffect(() => {
update.notification()
setHovered(false)
@@ -154,7 +154,7 @@ function UpdateNotification(props: { width: number }) {
update.open?.("notification")
}}
>
<FadeInText fg={theme.text.subdued} backdrop={backdrop()}>
<FadeInText fg={theme.text.muted} backdrop={backdrop()}>
<Show when={!remote}>
<span style={{ fg: theme.text.action.primary.selected }}>
{state.type === "installed" ? "/exit" : "/update"}
@@ -113,7 +113,7 @@ export function Composer(props: ComposerProps) {
<box
{...SplitBorder}
border={["left"]}
borderColor={theme.border.default}
borderColor={theme.border.base}
backgroundColor={theme.background.raised.base}
paddingLeft={1}
paddingRight={2}
@@ -125,7 +125,7 @@ export function Composer(props: ComposerProps) {
<Show
when={tabList().length > 1}
fallback={
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
{tabList()[0]?.label ?? ""}
</text>
}
@@ -136,7 +136,7 @@ export function Composer(props: ComposerProps) {
const isActive = createMemo(() => store.active === t.id)
return (
<text
fg={isActive() ? theme.text.default : theme.text.subdued}
fg={isActive() ? theme.text.base : theme.text.muted}
attributes={isActive() ? TextAttributes.BOLD : undefined}
>
{t.label}
@@ -146,7 +146,7 @@ export function Composer(props: ComposerProps) {
</For>
</box>
</Show>
<text fg={theme.text.subdued} onMouseUp={close}>
<text fg={theme.text.muted} onMouseUp={close}>
esc
</text>
</box>
@@ -159,19 +159,19 @@ export function Composer(props: ComposerProps) {
<For each={footerHints()}>
{(hint) => (
<text>
<span style={{ fg: theme.text.default }}>
<span style={{ fg: theme.text.base }}>
<b>{hint.label}</b>{" "}
</span>
<span style={{ fg: theme.text.subdued }}>{hint.shortcut}</span>
<span style={{ fg: theme.text.muted }}>{hint.shortcut}</span>
</text>
)}
</For>
<Show when={tabList().length > 1}>
<text>
<span style={{ fg: theme.text.default }}>
<span style={{ fg: theme.text.base }}>
<b>tabs</b>{" "}
</span>
<span style={{ fg: theme.text.subdued }}>/</span>
<span style={{ fg: theme.text.muted }}>/</span>
</text>
</Show>
</box>
@@ -113,7 +113,7 @@ export function ShellTab(props: { sessionID: string }) {
return (
<Show when={composer.active("shell")}>
<scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
<Show when={entries().length > 0} fallback={<text fg={theme.text.subdued}> No shell commands</text>}>
<Show when={entries().length > 0} fallback={<text fg={theme.text.muted}> No shell commands</text>}>
<For each={entries()}>
{(shell, index) => {
const active = createMemo(() => index() === store.selected)
@@ -123,7 +123,7 @@ export function ShellTab(props: { sessionID: string }) {
paddingLeft={1}
paddingRight={1}
backgroundColor={
active() ? theme.background.action.primary.focused : theme.background.action.primary.default
active() ? theme.background.action.primary.focused : theme.background.action.primary.base
}
onMouseMove={() => setStore("selected", index())}
onMouseUp={() => {
@@ -132,7 +132,7 @@ export function ShellTab(props: { sessionID: string }) {
}}
>
<text
fg={active() ? theme.text.action.primary.focused : theme.text.action.primary.default}
fg={active() ? theme.text.action.primary.focused : theme.text.action.primary.base}
attributes={active() ? TextAttributes.BOLD : undefined}
wrapMode="none"
>
@@ -194,7 +194,7 @@ export function SubagentsTab(props: { sessionID: string }) {
<scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
<Show
when={entries().length > 0}
fallback={<text fg={theme.text.subdued}> No {store.active ? "active" : "inactive"} subagents</text>}
fallback={<text fg={theme.text.muted}> No {store.active ? "active" : "inactive"} subagents</text>}
>
<For each={entries()}>
{(entry, index) => {
@@ -213,7 +213,7 @@ export function SubagentsTab(props: { sessionID: string }) {
? theme.background.action.primary.focused
: entry.current
? theme.background.action.primary.selected
: theme.background.action.primary.default
: theme.background.action.primary.base
}
onMouseMove={() => setStore("selected", index())}
onMouseUp={() => {
@@ -228,7 +228,7 @@ export function SubagentsTab(props: { sessionID: string }) {
? theme.text.action.primary.focused
: entry.current
? theme.text.action.primary.selected
: theme.text.action.primary.default
: theme.text.action.primary.base
}
attributes={active() ? TextAttributes.BOLD : undefined}
wrapMode="none"
@@ -238,7 +238,7 @@ export function SubagentsTab(props: { sessionID: string }) {
</text>
</box>
<Show when={status()}>
<text fg={active() ? theme.text.action.primary.focused : theme.text.subdued} wrapMode="none">
<text fg={active() ? theme.text.action.primary.focused : theme.text.muted} wrapMode="none">
{status()}
</text>
</Show>
@@ -82,7 +82,7 @@ export function TerminalsTab(props: { sessionID: string; visibleTerminalID?: str
? theme.background.action.primary.focused
: current()
? theme.background.action.primary.selected
: theme.background.action.primary.default
: theme.background.action.primary.base
}
onMouseMove={() => setSelected(index())}
onMouseUp={() => {
@@ -96,7 +96,7 @@ export function TerminalsTab(props: { sessionID: string; visibleTerminalID?: str
? theme.text.action.primary.focused
: current()
? theme.text.action.primary.selected
: theme.text.action.primary.default
: theme.text.action.primary.base
}
attributes={active() ? TextAttributes.BOLD : undefined}
wrapMode="none"
@@ -102,11 +102,11 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
return (
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" gap={2}>
<text fg={theme.text.default} attributes={TextAttributes.BOLD} flexGrow={1}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD} flexGrow={1}>
execute
</text>
<text fg={failed() ? theme.text.feedback.error.default : theme.text.subdued}>{status()}</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={failed() ? theme.text.feedback.error.base : theme.text.muted}>{status()}</text>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -119,22 +119,22 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
>
<box gap={1}>
<box>
<text fg={theme.text.subdued} attributes={TextAttributes.BOLD}>
<text fg={theme.text.muted} attributes={TextAttributes.BOLD}>
Code
</text>
<Show when={code()} fallback={<text fg={theme.text.subdued}>Waiting for code</text>}>
<Show when={code()} fallback={<text fg={theme.text.muted}>Waiting for code</text>}>
{(value) => <GutteredCode content={value()} filetype="typescript" digits={digits()} blocks={blocks} />}
</Show>
</box>
<box>
<text fg={theme.text.subdued} attributes={TextAttributes.BOLD}>
<text fg={theme.text.muted} attributes={TextAttributes.BOLD}>
Output
</text>
<Show
when={highlighted()}
fallback={
<text
fg={text() ? (failed() ? theme.text.feedback.error.default : theme.text.default) : theme.text.subdued}
fg={text() ? (failed() ? theme.text.feedback.error.base : theme.text.base) : theme.text.muted}
wrapMode="word"
>
{text() ?? (props.part.state.status === "completed" ? "No output" : "Waiting for output…")}
@@ -147,7 +147,7 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
<Show when={body().rest}>
{(rest) => (
<box paddingLeft={digits() + 1}>
<text fg={failed() ? theme.text.feedback.error.default : theme.text.default} wrapMode="word">
<text fg={failed() ? theme.text.feedback.error.base : theme.text.base} wrapMode="word">
{rest()}
</text>
</box>
@@ -160,20 +160,20 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
</box>
</scrollbox>
<box flexDirection="row" gap={3} flexWrap="wrap">
<text fg={theme.text.subdued}>/ / scroll</text>
<text fg={theme.text.muted}>/ / scroll</text>
<text onMouseUp={() => copy("code")}>
<span style={{ fg: copied() === "code" ? theme.text.feedback.success.default : theme.text.default }}>
<span style={{ fg: copied() === "code" ? theme.text.feedback.success.base : theme.text.base }}>
<b>{copied() === "code" ? "✓ copied" : "c"}</b>
</span>
<span style={{ fg: theme.text.subdued }}>{copied() === "code" ? "" : " copy code"}</span>
<span style={{ fg: theme.text.muted }}>{copied() === "code" ? "" : " copy code"}</span>
</text>
<text onMouseUp={() => copy("output")}>
<span style={{ fg: copied() === "output" ? theme.text.feedback.success.default : theme.text.default }}>
<span style={{ fg: copied() === "output" ? theme.text.feedback.success.base : theme.text.base }}>
<b>{copied() === "output" ? "✓ copied" : "o"}</b>
</span>
<span style={{ fg: theme.text.subdued }}>{copied() === "output" ? "" : " copy output"}</span>
<span style={{ fg: theme.text.muted }}>{copied() === "output" ? "" : " copy output"}</span>
</text>
<text fg={theme.text.subdued}>esc back</text>
<text fg={theme.text.muted}>esc back</text>
</box>
</box>
)
@@ -235,7 +235,7 @@ function GutteredCode(props: {
return (
<box flexDirection="row" gap={1} width="100%">
<text fg={theme.text.subdued} flexShrink={0} width={props.digits}>
<text fg={theme.text.muted} flexShrink={0} width={props.digits}>
{gutter()}
</text>
<box flexGrow={1} flexShrink={1} minWidth={0}>
@@ -244,7 +244,7 @@ function GutteredCode(props: {
width="100%"
conceal={false}
wrapMode="none"
fg={theme.text.default}
fg={theme.text.base}
filetype={props.filetype}
syntaxStyle={syntax()}
content={props.content}
+45 -45
View File
@@ -775,20 +775,20 @@ export function FormPrompt(props: { form: FormWithLocation }) {
>
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
<box paddingLeft={1}>
<text fg={theme.text.subdued}>{props.form.title}</text>
<text fg={theme.text.muted}>{props.form.title}</text>
</box>
<Show when={message()}>
<box paddingLeft={1}>
<text fg={theme.text.default}>{message()}</text>
<text fg={theme.text.base}>{message()}</text>
</box>
</Show>
<Show when={!single() && !tabbed()}>
<box flexDirection="row" gap={3} paddingLeft={1}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{confirm() ? "Review" : `Field ${Math.min(store.tab, fields().length - 1) + 1} of ${fields().length}`}
</text>
<Show when={fields().length > 0}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
· {answered()}/{fields().length} completed
</text>
</Show>
@@ -801,10 +801,10 @@ export function FormPrompt(props: { form: FormWithLocation }) {
const isTab = () => index() === store.tab
const color = () =>
isTab()
? theme.text.default
? theme.text.base
: tabHover() === index()
? theme.text.formfield.focused
: theme.text.subdued
: theme.text.muted
return (
<box
paddingRight={2}
@@ -847,10 +847,10 @@ export function FormPrompt(props: { form: FormWithLocation }) {
<text
fg={
confirm()
? theme.text.default
? theme.text.base
: tabHover() === "confirm"
? theme.text.formfield.focused
: theme.text.subdued
: theme.text.muted
}
attributes={confirm() ? TextAttributes.BOLD : undefined}
>
@@ -864,13 +864,13 @@ export function FormPrompt(props: { form: FormWithLocation }) {
{(external) => (
<box paddingLeft={1} gap={1}>
<Show when={external().title}>
<text fg={theme.text.default}>{external().title}</text>
<text fg={theme.text.base}>{external().title}</text>
</Show>
<Show when={external().description}>
<text fg={theme.text.subdued}>{external().description}</text>
<text fg={theme.text.muted}>{external().description}</text>
</Show>
<text
fg={theme.text.action.primary.default}
fg={theme.text.action.primary.base}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
openExternal()
@@ -879,7 +879,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
{external().url}
</text>
<text
fg={store.answers[external().key] === true ? theme.text.feedback.success.default : theme.text.subdued}
fg={store.answers[external().key] === true ? theme.text.feedback.success.base : theme.text.muted}
>
{store.answers[external().key] === true
? "✓ Acknowledged"
@@ -894,7 +894,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
<Show when={!confirm() && answerField()}>
<box paddingLeft={1} gap={1}>
<box>
<text fg={theme.text.default}>{answerField()!.description ?? formLabel(answerField()!)}</text>
<text fg={theme.text.base}>{answerField()!.description ?? formLabel(answerField()!)}</text>
</box>
<Show when={textual() ? answerField()!.key : undefined} keyed>
<box paddingLeft={1}>
@@ -913,12 +913,12 @@ export function FormPrompt(props: { form: FormWithLocation }) {
input() || formDisplayValue(answerField()!, store.answers[answerField()!.key], "(none)")
}
placeholder={placeholder()}
placeholderColor={theme.text.subdued}
placeholderColor={theme.text.muted}
minHeight={1}
maxHeight={6}
textColor={theme.text.default}
focusedTextColor={theme.text.default}
cursorColor={theme.text.default}
textColor={theme.text.base}
focusedTextColor={theme.text.base}
cursorColor={theme.text.base}
/>
</box>
</Show>
@@ -947,7 +947,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
paddingRight={1}
>
<text
fg={active() ? theme.text.formfield.focused : theme.text.subdued}
fg={active() ? theme.text.formfield.focused : theme.text.muted}
>{`${i() + 1}.`}</text>
</box>
<box
@@ -963,13 +963,13 @@ export function FormPrompt(props: { form: FormWithLocation }) {
? theme.text.formfield.focused
: picked()
? theme.text.formfield.selected
: theme.text.subdued
: theme.text.muted
}
>
[{picked() ? "✓" : " "}]
</text>
</Show>
<text fg={active() ? theme.text.formfield.focused : theme.text.formfield.default}>
<text fg={active() ? theme.text.formfield.focused : theme.text.formfield.base}>
{row.label}
</text>
</box>
@@ -979,7 +979,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
</box>
<Show when={row.description}>
<box paddingLeft={multi() ? 7 : 3}>
<text fg={theme.text.subdued}>{row.description}</text>
<text fg={theme.text.muted}>{row.description}</text>
</box>
</Show>
</box>
@@ -1000,7 +1000,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
backgroundColor={other() ? theme.background.formfield.focused : theme.background.raised.base}
paddingRight={1}
>
<text fg={other() ? theme.text.formfield.focused : theme.text.subdued}>
<text fg={other() ? theme.text.formfield.focused : theme.text.muted}>
{`${rows().length + 1}.`}
</text>
</box>
@@ -1018,7 +1018,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
? theme.text.formfield.focused
: customChecked()
? theme.text.formfield.selected
: theme.text.subdued
: theme.text.muted
}
>
[{customChecked() ? "✓" : " "}]
@@ -1028,7 +1028,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
when={store.editing}
fallback={
<>
<text fg={other() ? theme.text.formfield.focused : theme.text.formfield.default}>
<text fg={other() ? theme.text.formfield.focused : theme.text.formfield.base}>
{input() || "Type your own answer"}
</text>
<Show when={!multi() && customPicked()}>
@@ -1052,7 +1052,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
}}
initialValue={input()}
placeholder="Type your own answer"
placeholderColor={theme.text.subdued}
placeholderColor={theme.text.muted}
minHeight={1}
maxHeight={6}
textColor={theme.text.formfield.focused}
@@ -1093,12 +1093,12 @@ export function FormPrompt(props: { form: FormWithLocation }) {
return (
<box paddingLeft={1}>
<text>
<span style={{ fg: theme.text.subdued }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span style={{ fg: theme.text.muted }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span
style={{
fg: acknowledged()
? theme.text.feedback.success.default
: theme.text.feedback.error.default,
? theme.text.feedback.success.base
: theme.text.feedback.error.base,
}}
>
{acknowledged() ? "Acknowledged" : "(acknowledgement required)"}
@@ -1114,15 +1114,15 @@ export function FormPrompt(props: { form: FormWithLocation }) {
return (
<box paddingLeft={1}>
<text>
<span style={{ fg: theme.text.subdued }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span style={{ fg: theme.text.muted }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span
style={{
fg:
invalid() || missing()
? theme.text.feedback.error.default
? theme.text.feedback.error.base
: answered()
? theme.text.default
: theme.text.subdued,
? theme.text.base
: theme.text.muted,
}}
>
{invalid() ?? (answered() ? value() : missing() ? "(required)" : "(not answered)")}
@@ -1147,41 +1147,41 @@ export function FormPrompt(props: { form: FormWithLocation }) {
>
<box flexDirection="row" gap={2}>
<Show when={!single()}>
<text fg={theme.text.default}>
{"⇆"} <span style={{ fg: theme.text.subdued }}>tab</span>
<text fg={theme.text.base}>
{"⇆"} <span style={{ fg: theme.text.muted }}>tab</span>
</text>
</Show>
<Show when={!confirm() && !textual() && !externalField() && !store.editing}>
<text fg={theme.text.default}>
{"↑↓"} <span style={{ fg: theme.text.subdued }}>select</span>
<text fg={theme.text.base}>
{"↑↓"} <span style={{ fg: theme.text.muted }}>select</span>
</text>
</Show>
<Show when={confirm() && reviewContentHeight() > reviewMaxHeight()}>
<text fg={theme.text.default}>
{"↑↓"} <span style={{ fg: theme.text.subdued }}>scroll</span>
<text fg={theme.text.base}>
{"↑↓"} <span style={{ fg: theme.text.muted }}>scroll</span>
</text>
</Show>
<text
fg={theme.text.default}
fg={theme.text.base}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
if (confirm()) submit()
if (externalField()) acknowledgeExternal()
}}
>
enter <span style={{ fg: theme.text.subdued }}>{actionLabel()}</span>
enter <span style={{ fg: theme.text.muted }}>{actionLabel()}</span>
</text>
<Show when={externalField()}>
<text fg={theme.text.default} onMouseUp={copyExternal}>
c <span style={{ fg: theme.text.subdued }}>copy</span>
<text fg={theme.text.base} onMouseUp={copyExternal}>
c <span style={{ fg: theme.text.muted }}>copy</span>
</text>
</Show>
<text fg={theme.text.default} onMouseUp={cancel}>
esc <span style={{ fg: theme.text.subdued }}>{store.editing && !textual() ? "close" : "dismiss"}</span>
<text fg={theme.text.base} onMouseUp={cancel}>
esc <span style={{ fg: theme.text.muted }}>{store.editing && !textual() ? "close" : "dismiss"}</span>
</text>
</box>
<Show when={store.error}>
<text fg={theme.text.feedback.error.default}>{store.error}</text>
<text fg={theme.text.feedback.error.base}>{store.error}</text>
</Show>
</box>
</box>
+98 -98
View File
@@ -1293,7 +1293,7 @@ export function Session(props: {
visible: showScrollbar(),
trackOptions: {
backgroundColor: theme.decrease(theme.background.raised.base),
foregroundColor: theme.border.default,
foregroundColor: theme.border.base,
},
}}
stickyScroll={!navigationMessage()}
@@ -1330,7 +1330,7 @@ export function Session(props: {
</box>
<box height={1} flexShrink={0} flexDirection="row" justifyContent="flex-end">
<Show when={firstJump()}>
<text fg={theme.text.feedback.info.default}>Loading session history</text>
<text fg={theme.text.feedback.info.base}>Loading session history</text>
</Show>
<Show when={!firstJump() && awayFromBottom()}>
<box
@@ -1341,7 +1341,7 @@ export function Session(props: {
onMouseUp={toBottom}
>
<text
fg={latestHovered() ? theme.text.action.secondary.hovered : theme.text.action.secondary.default}
fg={latestHovered() ? theme.text.action.secondary.hovered : theme.text.action.secondary.base}
>
Jump to latest
</text>
@@ -1550,7 +1550,7 @@ function TurnTokenUsage(props: {
setExpanded((value) => !value)
}}
>
<text fg={hover() ? theme.text.default : theme.text.subdued} wrapMode="none">
<text fg={hover() ? theme.text.base : theme.text.muted} wrapMode="none">
<span>{expanded() ? "- " : "+ "}</span>
<span style={{ attributes: TextAttributes.BOLD }}>Tokens</span>
<span>
@@ -1558,7 +1558,7 @@ function TurnTokenUsage(props: {
new · {summary().cached.toLocaleString()} cached · {summary().total.toLocaleString()} total
</span>
<Show when={summary().reuseDrops > 0}>
<span style={{ fg: theme.text.feedback.warning.default }}>
<span style={{ fg: theme.text.feedback.warning.base }}>
{" "}
· ! {summary().reuseDrops} likely cache {summary().reuseDrops === 1 ? "bust" : "busts"}
</span>
@@ -1567,7 +1567,7 @@ function TurnTokenUsage(props: {
</box>
<Show when={expanded()}>
<box paddingLeft={INLINE_TOOL_ICON_WIDTH}>
<text fg={theme.text.subdued} attributes={TextAttributes.ITALIC}>
<text fg={theme.text.muted} attributes={TextAttributes.ITALIC}>
{"Step".padEnd(columns().step + 2)}
{"New".padStart(columns().newTokens)}
{" "}
@@ -1579,7 +1579,7 @@ function TurnTokenUsage(props: {
<For each={steps()}>
{(item) => (
<box paddingLeft={INLINE_TOOL_ICON_WIDTH} flexDirection="column">
<text fg={verbose() && item.finish === "tool-call" ? undefined : theme.text.subdued}>
<text fg={verbose() && item.finish === "tool-call" ? undefined : theme.text.muted}>
{item.finish.padEnd(columns().step + 2)}
<span style={{ attributes: TextAttributes.BOLD }}>
{item.newTokens.toLocaleString().padStart(columns().newTokens)}
@@ -1591,7 +1591,7 @@ function TurnTokenUsage(props: {
</text>
<TurnTokenToolCalls tools={item.tools} />
<Show when={item.reuseDrop !== undefined}>
<text fg={theme.text.feedback.warning.default}>
<text fg={theme.text.feedback.warning.base}>
! Likely cache bust: {item.reuseDrop?.toLocaleString()} fewer cached tokens than the previous step
</text>
</Show>
@@ -1613,10 +1613,10 @@ function TurnTokenToolCalls(props: { tools: SessionMessageAssistantTool[] }) {
<For each={props.tools}>
{(tool) => (
<box flexDirection="row">
<text width={nameWidth()} flexShrink={0} fg={theme.text.subdued} attributes={TextAttributes.BOLD}>
<text width={nameWidth()} flexShrink={0} fg={theme.text.muted} attributes={TextAttributes.BOLD}>
{tool.name}
</text>
<text fg={theme.text.subdued} attributes={TextAttributes.DIM} wrapMode="word" flexGrow={1} minWidth={0}>
<text fg={theme.text.muted} attributes={TextAttributes.DIM} wrapMode="word" flexGrow={1} minWidth={0}>
{turnTokenToolSummary(tool)}
</text>
</box>
@@ -1664,8 +1664,8 @@ function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) {
<Show when={visible() && shortcut()}>
{(value) => (
<box marginTop={1} paddingLeft={3} flexShrink={0}>
<text fg={theme.text.subdued}>
Press <span style={{ fg: theme.text.default }}>{value()}</span> to move running work to the background
<text fg={theme.text.muted}>
Press <span style={{ fg: theme.text.base }}>{value()}</span> to move running work to the background
</text>
</box>
)}
@@ -1747,7 +1747,7 @@ function SessionReasoningGroupView(props: {
const ctx = use()
const theme = useTheme()
const { currentSyntax: syntax } = useThemes()
const thinkingSyntax = createSyntaxStyleMemo(() => generateThinkingSyntax(syntax(), theme.text.subdued))
const thinkingSyntax = createSyntaxStyleMemo(() => generateThinkingSyntax(syntax(), theme.text.muted))
const renderer = useRenderer()
const [expanded, setExpanded] = createSignal(false)
const [hover, setHover] = createSignal(false)
@@ -1787,13 +1787,13 @@ function SessionReasoningGroupView(props: {
icon={expanded() ? "-" : "+"}
color={
!props.completed
? theme.text.default
? theme.text.base
: hover() || expanded()
? theme.text.feedback.warning.default
? theme.text.feedback.warning.base
: RGBA.fromValues(
theme.text.feedback.warning.default.r,
theme.text.feedback.warning.default.g,
theme.text.feedback.warning.default.b,
theme.text.feedback.warning.base.r,
theme.text.feedback.warning.base.g,
theme.text.feedback.warning.base.b,
0.6,
)
}
@@ -1846,7 +1846,7 @@ function SessionReasoningGroupView(props: {
syntaxStyle={thinkingSyntax()}
content={content()}
conceal={ctx.markdownMode() === "rendered"}
fg={theme.text.subdued}
fg={theme.text.muted}
/>
</box>
</box>
@@ -1907,7 +1907,7 @@ function SessionGroupView(props: {
<Show when={grouped().length > 0}>
<InlineToolRow
icon={completed() ? "→" : "✱"}
color={hover() ? theme.text.default : theme.text.subdued}
color={hover() ? theme.text.base : theme.text.muted}
complete={completed()}
pending={label()}
spinner={!completed()}
@@ -1956,26 +1956,26 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
<>
<Show when={props.message.error && !interrupted() && !props.message.retry}>
<box paddingLeft={3}>
<text fg={theme.text.feedback.error.default}>Error: {errorMessage(props.message.error)}</text>
<text fg={theme.text.feedback.error.base}>Error: {errorMessage(props.message.error)}</text>
</box>
</Show>
<AssistantRetry retry={props.message.retry} />
<box paddingLeft={3} marginTop={props.message.retry || (props.message.error && !interrupted()) ? 1 : 0}>
<text>
<span style={{ fg: props.message.error ? theme.text.subdued : local.agent.color(props.message.agent) }}>
<span style={{ fg: props.message.error ? theme.text.muted : local.agent.color(props.message.agent) }}>
{Locale.titlecase(props.message.agent)}
</span>
<Show when={ctx.terminal.width >= 28}>
<span style={{ fg: theme.text.subdued }}> · {model()}</span>
<span style={{ fg: theme.text.muted }}> · {model()}</span>
</Show>
<Show when={duration() && (ctx.terminal.width < 28 || ctx.terminal.width >= 36)}>
<span style={{ fg: theme.text.subdued }}> · {Locale.duration(duration())}</span>
<span style={{ fg: theme.text.muted }}> · {Locale.duration(duration())}</span>
</Show>
<Show when={config.data.session.tps && tokensPerSecond()}>
{(value) => <span style={{ fg: theme.text.subdued }}> · {value().toFixed(1)} tok/s</span>}
{(value) => <span style={{ fg: theme.text.muted }}> · {value().toFixed(1)} tok/s</span>}
</Show>
<Show when={interrupted()}>
<span style={{ fg: theme.text.subdued }}> · interrupted</span>
<span style={{ fg: theme.text.muted }}> · interrupted</span>
</Show>
</text>
</box>
@@ -1990,8 +1990,8 @@ function SessionSwitchMessageV2(props: { message: SessionMessageInfo }) {
return (
<box paddingLeft={3}>
<text>
<span style={{ fg: theme.text.subdued }}> Moved to </span>
<span style={{ fg: theme.text.feedback.info.default }}>{props.message.location.directory}</span>
<span style={{ fg: theme.text.muted }}> Moved to </span>
<span style={{ fg: theme.text.feedback.info.base }}>{props.message.location.directory}</span>
</text>
</box>
)
@@ -2008,7 +2008,7 @@ function SessionSwitchMessageV2(props: { message: SessionMessageInfo }) {
}
return (
<box paddingLeft={3}>
<text fg={theme.text.subdued}>{text()}</text>
<text fg={theme.text.muted}>{text()}</text>
</box>
)
}
@@ -2039,16 +2039,16 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
const heading = () => `${state() === "completed" ? "↳" : "!"} ${actor()} ${status()}`
const suffix = () => Locale.truncateWidth(` · ${description()}`, Math.max(0, ctx.width - 3 - stringWidth(heading())))
const color = () => {
if (state() === "error") return theme.text.feedback.error.default
if (state() === "cancelled") return theme.text.feedback.warning.default
if (hover() && childID()) return theme.text.default
return theme.text.feedback.info.default
if (state() === "error") return theme.text.feedback.error.base
if (state() === "cancelled") return theme.text.feedback.warning.base
if (hover() && childID()) return theme.text.base
return theme.text.feedback.info.base
}
return (
<Show
when={completion()}
fallback={
<InlineToolRow icon="◈" color={theme.text.subdued} pending="Notice" complete={true}>
<InlineToolRow icon="◈" color={theme.text.muted} pending="Notice" complete={true}>
{text()}
</InlineToolRow>
}
@@ -2065,7 +2065,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
>
<text wrapMode="none">
<span style={{ fg: color() }}>{heading()}</span>
<span style={{ fg: theme.text.subdued }}>{suffix()}</span>
<span style={{ fg: theme.text.muted }}>{suffix()}</span>
</text>
</box>
</Show>
@@ -2075,7 +2075,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
function SessionSkillMessage(props: { message: Extract<SessionMessageInfo, { type: "skill" }> }) {
const theme = useTheme()
return (
<InlineToolRow icon="→" color={theme.text.subdued} pending="Skill" complete={true}>
<InlineToolRow icon="→" color={theme.text.muted} pending="Skill" complete={true}>
Skill {props.message.name}
</InlineToolRow>
)
@@ -2091,7 +2091,7 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
const text = () =>
props.message.status === "failed" ? (cancelled() ? "" : props.message.error.message) : props.message.summary
const content = createMemo(() => text().trim())
const color = () => (status() === "failed" && !cancelled() ? theme.text.feedback.error.default : theme.text.subdued)
const color = () => (status() === "failed" && !cancelled() ? theme.text.feedback.error.base : theme.text.muted)
// Usage of the compaction request itself; the resulting context size only shows on the next assistant step.
const usage = () => {
if (props.message.status === "running" || !props.message.tokens) return
@@ -2141,7 +2141,7 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
tableOptions={{ style: "grid", cellPaddingX: 1 }}
conceal={ctx.markdownMode() === "rendered"}
fg={theme.markdown.text}
bg={theme.background.default}
bg={theme.background.base}
/>
</box>
</Show>
@@ -2153,12 +2153,12 @@ function CompactionQueued() {
const theme = useTheme()
return (
<box flexDirection="row" alignItems="center">
<box border={["top"]} borderColor={theme.border.default} flexGrow={1} />
<box border={["top"]} borderColor={theme.border.base} flexGrow={1} />
<box flexDirection="row" gap={1} paddingLeft={1} paddingRight={1}>
<text fg={theme.text.subdued}></text>
<text fg={theme.text.subdued}>Compaction queued</text>
<text fg={theme.text.muted}></text>
<text fg={theme.text.muted}>Compaction queued</text>
</box>
<box border={["top"]} borderColor={theme.border.default} flexGrow={1} />
<box border={["top"]} borderColor={theme.border.base} flexGrow={1} />
</box>
)
}
@@ -2212,7 +2212,7 @@ function RevertMessage(props: {
paddingLeft={2}
backgroundColor={hover() ? theme.decrease(theme.background.raised.base) : theme.background.raised.base}
>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{props.count} message{props.count === 1 ? "" : "s"} reverted
</text>
<Show when={props.files.length > 0}>
@@ -2220,7 +2220,7 @@ function RevertMessage(props: {
<For each={props.files}>
{(file) => (
<box flexDirection="row" gap={1} flexShrink={0}>
<text fg={theme.text.subdued}>{statusLabel(file.status)}</text>
<text fg={theme.text.muted}>{statusLabel(file.status)}</text>
<FilePath
value={file.file}
maxWidth={Math.max(
@@ -2230,7 +2230,7 @@ function RevertMessage(props: {
(file.additions > 0 ? stringWidth(`+${file.additions}`) + 1 : 0) -
(file.deletions > 0 ? stringWidth(`-${file.deletions}`) + 1 : 0),
)}
fg={theme.text.default}
fg={theme.text.base}
/>
<Show when={file.additions > 0}>
<text fg={theme.diff.text.added}>+{file.additions}</text>
@@ -2243,8 +2243,8 @@ function RevertMessage(props: {
</For>
</box>
</Show>
<text fg={theme.text.subdued}>
<span style={{ fg: theme.text.default }}>{redoKey()}</span> or /redo to restore
<text fg={theme.text.muted}>
<span style={{ fg: theme.text.base }}>{redoKey()}</span> or /redo to restore
</text>
</box>
</box>
@@ -2299,7 +2299,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
<Show when={props.message.text.trim() || files().length || skills().length}>
<box
border={["left"]}
borderColor={delivery() ? theme.border.default : color()}
borderColor={delivery() ? theme.border.base : color()}
customBorderChars={SplitBorder.customBorderChars}
backgroundColor={theme.background.raised.base}
>
@@ -2342,12 +2342,12 @@ function UserMessage(props: { message: SessionMessageUser }) {
backgroundColor={hover() ? theme.decrease(theme.background.raised.base) : theme.background.raised.base}
flexShrink={0}
>
<text fg={theme.text.default}>{props.message.text}</text>
<text fg={theme.text.base}>{props.message.text}</text>
<Show when={skills().length}>
<box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
<For each={skills()}>
{(skill) => (
<text fg={theme.text.default}>
<text fg={theme.text.base}>
<span
style={{
bg: theme.hue.accent[mode() === "light" ? 300 : 200],
@@ -2357,7 +2357,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
>
{" skill "}
</span>
<span style={{ bg: theme.decrease(theme.background.raised.base), fg: theme.text.subdued }}>
<span style={{ bg: theme.decrease(theme.background.raised.base), fg: theme.text.muted }}>
{` ${skill.name} `}
</span>
</text>
@@ -2371,7 +2371,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
{(file) => {
const label = file.mime === "application/x-directory" ? "dir" : "file"
return (
<text fg={theme.text.default}>
<text fg={theme.text.base}>
<span
style={{
bg: theme.hue.accent[mode() === "light" ? 300 : 200],
@@ -2381,7 +2381,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
>
{` ${label} `}
</span>
<span style={{ bg: theme.decrease(theme.background.raised.base), fg: theme.text.subdued }}>
<span style={{ bg: theme.decrease(theme.background.raised.base), fg: theme.text.muted }}>
{" "}
{file.name ?? (file.source.type === "uri" ? file.source.uri : "attachment")}{" "}
</span>
@@ -2405,7 +2405,7 @@ function QueuedPromptDock(props: { prompts: { id: string; text: string }[]; onOp
return (
<box
border={["left"]}
borderColor={theme.border.default}
borderColor={theme.border.base}
customBorderChars={SplitBorder.customBorderChars}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
@@ -2420,8 +2420,8 @@ function QueuedPromptDock(props: { prompts: { id: string; text: string }[]; onOp
backgroundColor={hover() ? theme.decrease(theme.background.raised.base) : theme.background.raised.base}
flexDirection="row"
>
<text fg={theme.text.subdued} wrapMode="none" truncate flexGrow={1} flexShrink={1} minWidth={0}>
<span style={{ fg: theme.text.default }}>{props.prompts.length} queued</span>
<text fg={theme.text.muted} wrapMode="none" truncate flexGrow={1} flexShrink={1} minWidth={0}>
<span style={{ fg: theme.text.base }}>{props.prompts.length} queued</span>
<Show when={next()}>{(text) => <> · {text()}</>}</Show>
</text>
</box>
@@ -2446,7 +2446,7 @@ function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
<Show when={props.retry}>
{(retry) => (
<box paddingLeft={3}>
<text fg={theme.text.feedback.warning.default}>
<text fg={theme.text.feedback.warning.base}>
{seconds() > 0 ? `Retrying in ${seconds()}s` : "Retry due"} · attempt {retry().attempt} ·{" "}
{retry().error.message}
</text>
@@ -2642,10 +2642,10 @@ function GenericTool(props: ToolProps) {
<For each={input()}>
{([key, value]) => (
<box flexDirection="row">
<text flexShrink={0} fg={theme.text.subdued}>
<text flexShrink={0} fg={theme.text.muted}>
{key}:{" "}
</text>
<text flexGrow={1} wrapMode="word" fg={theme.text.default}>
<text flexGrow={1} wrapMode="word" fg={theme.text.base}>
{typeof value === "string" ? value : JSON.stringify(value, null, 2)}
</text>
</box>
@@ -2654,10 +2654,10 @@ function GenericTool(props: ToolProps) {
<Show when={output()}>
{(value) => (
<box flexDirection="row">
<text flexShrink={0} fg={theme.text.subdued}>
<text flexShrink={0} fg={theme.text.muted}>
output:{" "}
</text>
<text flexGrow={1} fg={theme.text.default} wrapMode="word">
<text flexGrow={1} fg={theme.text.base} wrapMode="word">
{value()}
</text>
</box>
@@ -2721,10 +2721,10 @@ function InlineTool(props: {
const clickable = createMemo(() => Boolean(props.onClick || failed()))
const fg = createMemo(() => {
if (props.color) return props.color
if (permission()) return theme.text.feedback.warning.default
if (failed()) return theme.text.feedback.error.default
if (hover() && props.onClick) return theme.text.default
return theme.text.subdued
if (permission()) return theme.text.feedback.warning.base
if (failed()) return theme.text.feedback.error.base
if (hover() && props.onClick) return theme.text.base
return theme.text.muted
})
return (
@@ -2732,7 +2732,7 @@ function InlineTool(props: {
icon={props.icon}
iconColor={props.iconColor}
color={fg()}
errorColor={theme.text.feedback.error.default}
errorColor={theme.text.feedback.error.base}
failed={failed()}
denied={Boolean(denied())}
error={error()}
@@ -2760,9 +2760,9 @@ function InlineTool(props: {
function StatusBadge(props: { children: string; raised?: boolean }) {
const theme = useTheme()
const background = () => (props.raised ? theme.background.raised.base : theme.background.default)
const background = () => (props.raised ? theme.background.raised.base : theme.background.base)
return (
<text flexShrink={0} bg={theme.decrease(background())} fg={theme.text.subdued}>
<text flexShrink={0} bg={theme.decrease(background())} fg={theme.text.muted}>
{" "}
{props.children}{" "}
</text>
@@ -2801,7 +2801,7 @@ function BlockTool(props: BlockToolProps) {
gap={1}
backgroundColor={hover() ? theme.decrease(background()) : background()}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.background.default}
borderColor={theme.background.base}
onMouseOver={() => props.onClick && setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => {
@@ -2818,13 +2818,13 @@ function BlockTool(props: BlockToolProps) {
when={props.spinner}
fallback={
<text
fg={permission() ? theme.text.feedback.warning.default : (props.headerColor ?? theme.text.subdued)}
fg={permission() ? theme.text.feedback.warning.base : (props.headerColor ?? theme.text.muted)}
>
{title()}
</text>
}
>
<Spinner color={permission() ? theme.text.feedback.warning.default : theme.text.subdued}>
<Spinner color={permission() ? theme.text.feedback.warning.base : theme.text.muted}>
{title().replace(/^# /, "")}
</Spinner>
</Show>
@@ -2839,27 +2839,27 @@ function BlockTool(props: BlockToolProps) {
fallback={
<text
flexShrink={0}
fg={permission() ? theme.text.feedback.warning.default : (props.headerColor ?? theme.text.subdued)}
fg={permission() ? theme.text.feedback.warning.base : (props.headerColor ?? theme.text.muted)}
>
{path().label}
</text>
}
>
<Spinner color={permission() ? theme.text.feedback.warning.default : theme.text.subdued}>
<Spinner color={permission() ? theme.text.feedback.warning.base : theme.text.muted}>
{path().label.replace(/^# /, "")}
</Spinner>
</Show>
<FilePath
value={path().value}
maxWidth={Math.max(2, ctx.width - 4 - stringWidth(path().label) - (props.spinner ? 2 : 0))}
fg={permission() ? theme.text.feedback.warning.default : (props.headerColor ?? theme.text.subdued)}
fg={permission() ? theme.text.feedback.warning.base : (props.headerColor ?? theme.text.muted)}
/>
</box>
)}
</Show>
{props.children}
<Show when={error()}>
<text fg={props.errorColor ?? theme.text.feedback.error.default}>{error()}</text>
<text fg={props.errorColor ?? theme.text.feedback.error.base}>{error()}</text>
</Show>
</box>
)
@@ -2899,7 +2899,7 @@ function ShellDisplay(props: {
// A Session can move while its shell is still running in the original Location.
const location = data.shell.get(props.shellID ?? "")?.location ?? data.session.get(ctx.sessionID)?.location
const permission = useToolPermission(() => props.part)
const color = createMemo(() => (permission() ? theme.text.feedback.warning.default : theme.text.default))
const color = createMemo(() => (permission() ? theme.text.feedback.warning.base : theme.text.base))
const backgroundRunning = createMemo(() => {
const id = props.shellID
return Boolean(id && data.shell.get(id))
@@ -3000,7 +3000,7 @@ function ShellDisplay(props: {
isRunning() || props.status === "streaming" ? (
<Spinner color={color()}>Writing command</Spinner>
) : (
<text fg={theme.text.subdued}>Writing command</text>
<text fg={theme.text.muted}>Writing command</text>
)
}
>
@@ -3008,7 +3008,7 @@ function ShellDisplay(props: {
when={isRunning()}
fallback={
<text
fg={theme.text.default}
fg={theme.text.base}
wrapMode={expanded() ? "word" : "char"}
maxHeight={expanded() ? undefined : 2}
>
@@ -3030,7 +3030,7 @@ function ShellDisplay(props: {
</box>
</Show>
<Show when={limitedOutput()}>
<text fg={theme.text.subdued}>{limitedOutput()}</text>
<text fg={theme.text.muted}>{limitedOutput()}</text>
</Show>
</Show>
<Show when={props.background}>
@@ -3056,10 +3056,10 @@ function Write(props: ToolProps) {
path={{ label: "# Wrote", value: pathFormatter.format(stringValue(props.input.path)) }}
part={props.part}
>
<line_number fg={theme.text.subdued} minWidth={3} paddingRight={1}>
<line_number fg={theme.text.muted} minWidth={3} paddingRight={1}>
<code
conceal={false}
fg={theme.text.default}
fg={theme.text.base}
filetype={filetype(stringValue(props.input.path))}
syntaxStyle={syntax()}
content={code()}
@@ -3114,7 +3114,7 @@ function Read(props: ToolProps) {
<For each={loaded()}>
{(filepath) => (
<box paddingLeft={3}>
<text paddingLeft={3} fg={theme.text.subdued}>
<text paddingLeft={3} fg={theme.text.muted}>
Loaded {pathFormatter.format(filepath)}
</text>
</box>
@@ -3234,11 +3234,11 @@ function ExecuteCallView(props: { call: Accessor<ExecuteCall> }) {
const [hover, setHover] = createSignal(false)
const input = createMemo(() => Object.entries(props.call().input ?? {}))
const expandable = createMemo(() => input().length > 0)
const expandedColor = createMemo(() => theme.decrease(theme.text.subdued))
const expandedColor = createMemo(() => theme.decrease(theme.text.muted))
const color = createMemo(() => {
if (props.call().status === "error") return theme.text.feedback.error.default
if (hover()) return theme.text.default
return expanded() ? expandedColor() : theme.text.subdued
if (props.call().status === "error") return theme.text.feedback.error.base
if (hover()) return theme.text.base
return expanded() ? expandedColor() : theme.text.muted
})
return (
@@ -3264,10 +3264,10 @@ function ExecuteCallView(props: { call: Accessor<ExecuteCall> }) {
<For each={input()}>
{([key, value]) => (
<box flexDirection="row">
<text flexShrink={0} fg={theme.text.subdued}>
<text flexShrink={0} fg={theme.text.muted}>
{key}:{" "}
</text>
<text flexGrow={1} wrapMode="word" fg={theme.text.default}>
<text flexGrow={1} wrapMode="word" fg={theme.text.base}>
{typeof value === "string" ? value : JSON.stringify(value, null, 2)}
</text>
</box>
@@ -3295,7 +3295,7 @@ function Execute(props: ToolProps) {
<>
<InlineTool
icon={hasRuntimeError() ? "✗" : props.part.state.status === "completed" ? "✓" : "│"}
color={hasRuntimeError() ? theme.text.feedback.error.default : undefined}
color={hasRuntimeError() ? theme.text.feedback.error.base : undefined}
spinner={isLoading()}
pending="execute"
complete={true}
@@ -3309,7 +3309,7 @@ function Execute(props: ToolProps) {
<box paddingLeft={3}>
<For each={outputPreview().split("\n")}>
{(line, index) => (
<text paddingLeft={3} fg={theme.text.feedback.error.default}>
<text paddingLeft={3} fg={theme.text.feedback.error.base}>
{index() === 0 ? "↳ " : " "}
{line}
</text>
@@ -3353,7 +3353,7 @@ function Edit(props: ToolProps) {
showLineNumbers={true}
width="100%"
wrapMode={ctx.diffWrapMode()}
fg={theme.text.default}
fg={theme.text.base}
addedBg={theme.diff.background.added}
removedBg={theme.diff.background.removed}
contextBg={theme.diff.background.context}
@@ -3443,7 +3443,7 @@ function ApplyPatch(props: ToolProps) {
showLineNumbers={true}
width="100%"
wrapMode={ctx.diffWrapMode()}
fg={theme.text.default}
fg={theme.text.base}
addedBg={theme.diff.background.added}
removedBg={theme.diff.background.removed}
contextBg={theme.diff.background.context}
@@ -3475,7 +3475,7 @@ function ApplyPatch(props: ToolProps) {
<FilePath
value={file.resource}
maxWidth={Math.max(2, ctx.width - 3)}
fg={file.type === "delete" ? theme.diff.text.removed : theme.text.subdued}
fg={file.type === "delete" ? theme.diff.text.removed : theme.text.muted}
/>
</BlockTool>
)}
@@ -3497,8 +3497,8 @@ function ApplyPatch(props: ToolProps) {
}
part={props.part}
spinner={props.part.state.status === "streaming" || props.part.state.status === "running"}
headerColor={props.part.state.status === "error" ? theme.text.feedback.error.default : undefined}
errorColor={props.part.state.status === "error" ? theme.text.subdued : undefined}
headerColor={props.part.state.status === "error" ? theme.text.feedback.error.base : undefined}
errorColor={props.part.state.status === "error" ? theme.text.muted : undefined}
/>
</Match>
</Switch>
@@ -3524,8 +3524,8 @@ function Question(props: ToolProps) {
<For each={questions()}>
{(q, i) => (
<box flexDirection="column">
<text fg={theme.text.subdued}>{q.question}</text>
<text fg={theme.text.default}>{format(answers()?.[i()])}</text>
<text fg={theme.text.muted}>{q.question}</text>
<text fg={theme.text.base}>{format(answers()?.[i()])}</text>
</box>
)}
</For>
@@ -3566,7 +3566,7 @@ function Diagnostics(props: { diagnostics: unknown; filePath: string }) {
<box>
<For each={errors()}>
{(diagnostic) => (
<text fg={theme.text.feedback.error.default}>
<text fg={theme.text.feedback.error.base}>
Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}] {diagnostic.message}
</text>
)}
@@ -25,8 +25,8 @@ export function SessionLocationUnavailable(props: { directory: string; onMove: (
title="Session location unavailable"
body={
<box paddingLeft={1} gap={1}>
<text fg={theme.text.subdued}>{directory()}</text>
<text fg={theme.text.default}>Choose another directory to continue this session.</text>
<text fg={theme.text.muted}>{directory()}</text>
<text fg={theme.text.base}>Choose another directory to continue this session.</text>
</box>
}
options={{ move: "Choose directory" }}
@@ -24,7 +24,7 @@ export function ReasoningPart(props: {
}) {
const theme = useTheme()
const { currentSyntax: syntax } = useThemes()
const thinkingSyntax = createSyntaxStyleMemo(() => generateThinkingSyntax(syntax(), theme.text.subdued))
const thinkingSyntax = createSyntaxStyleMemo(() => generateThinkingSyntax(syntax(), theme.text.muted))
const ctx = use()
// Collapsed by default in hide mode: a single line throughout, so the
// layout never shifts. Click to open the full markdown block, click to close.
@@ -52,7 +52,7 @@ export function ReasoningPart(props: {
<box
border={!inMinimal() || expanded() ? ["left"] : undefined}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.decrease(theme.background.default)}
borderColor={theme.decrease(theme.background.base)}
paddingLeft={!inMinimal() || expanded() ? 1 : 0}
>
<box onMouseUp={toggle}>
@@ -70,7 +70,7 @@ export function ReasoningPart(props: {
<box
border={["left"]}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.decrease(theme.background.default)}
borderColor={theme.decrease(theme.background.base)}
paddingLeft={inMinimal() ? 3 : 1}
>
<code
@@ -80,7 +80,7 @@ export function ReasoningPart(props: {
syntaxStyle={thinkingSyntax()}
content={content()}
conceal={ctx.markdownMode() === "rendered"}
fg={theme.text.subdued}
fg={theme.text.muted}
/>
</box>
</box>
@@ -106,12 +106,12 @@ function ReasoningHeader(props: {
const fg = () =>
props.open
? RGBA.fromValues(
theme.text.feedback.warning.default.r,
theme.text.feedback.warning.default.g,
theme.text.feedback.warning.default.b,
theme.text.feedback.warning.base.r,
theme.text.feedback.warning.base.g,
theme.text.feedback.warning.base.b,
0.6,
)
: theme.text.feedback.warning.default
: theme.text.feedback.warning.base
return (
<Switch>
@@ -166,7 +166,7 @@ export function TextPart(props: {
tableOptions={{ style: "grid", cellPaddingX: 1 }}
conceal={ctx.markdownMode() === "rendered"}
fg={theme.markdown.text}
bg={theme.background.default}
bg={theme.background.base}
/>
</box>
</Show>
+37 -37
View File
@@ -47,8 +47,8 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
scrollAcceleration={scrollAcceleration()}
verticalScrollbarOptions={{
trackOptions: {
backgroundColor: theme.background.default,
foregroundColor: theme.scrollbar.default,
backgroundColor: theme.background.base,
foregroundColor: theme.scrollbar.base,
},
}}
>
@@ -61,7 +61,7 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
showLineNumbers={true}
width="100%"
wrapMode="word"
fg={theme.text.default}
fg={theme.text.base}
addedBg={theme.diff.background.added}
removedBg={theme.diff.background.removed}
contextBg={theme.diff.background.context}
@@ -79,7 +79,7 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
when={props.patch}
fallback={
<box paddingLeft={1}>
<text fg={theme.text.subdued}>No diff provided</text>
<text fg={theme.text.muted}>No diff provided</text>
</box>
}
>
@@ -89,8 +89,8 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
scrollAcceleration={scrollAcceleration()}
verticalScrollbarOptions={{
trackOptions: {
backgroundColor: theme.background.default,
foregroundColor: theme.scrollbar.default,
backgroundColor: theme.background.base,
foregroundColor: theme.scrollbar.base,
},
}}
>
@@ -100,7 +100,7 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
streaming={true}
syntaxStyle={syntax()}
content={patch()}
fg={theme.text.subdued}
fg={theme.text.muted}
/>
</scrollbox>
)}
@@ -171,9 +171,9 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
) : props.request.action === "external_directory" ? (
<Show when={current.lines.length > 0}>
<box paddingLeft={1} gap={1}>
<text fg={theme.text.subdued}>Patterns</text>
<text fg={theme.text.muted}>Patterns</text>
<box>
<For each={current.lines}>{(line) => <text fg={theme.text.default}>{line}</text>}</For>
<For each={current.lines}>{(line) => <text fg={theme.text.base}>{line}</text>}</For>
</box>
</box>
</Show>
@@ -186,8 +186,8 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
props.request.action === "shell" ||
props.request.action === "subagent" ||
props.request.action === "task"
? theme.text.default
: theme.text.subdued
? theme.text.base
: theme.text.muted
}
>
{line}
@@ -200,15 +200,15 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
const header = () => (
<box flexDirection="column" gap={0}>
<box flexDirection="row" gap={1} flexShrink={0}>
<text fg={theme.text.feedback.warning.default}>{"△"}</text>
<text fg={theme.text.default}>Permission required</text>
<text fg={theme.text.feedback.warning.base}>{"△"}</text>
<text fg={theme.text.base}>Permission required</text>
</box>
<Show when={props.request.action !== "shell" && current.title}>
<box flexDirection="row" gap={1} paddingLeft={2} flexShrink={0}>
<text fg={theme.text.subdued} flexShrink={0}>
<text fg={theme.text.muted} flexShrink={0}>
{current.icon}
</text>
<text fg={theme.text.default}>{current.title}</text>
<text fg={theme.text.base}>{current.title}</text>
</box>
</Show>
</box>
@@ -224,7 +224,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
<Show when={option === "always"} fallback={presentationBody()}>
<box paddingLeft={1} gap={1}>
<For each={permissionAlwaysLines(props.request)}>
{(line, index) => <text fg={index() === 0 ? theme.text.subdued : theme.text.default}>{line}</text>}
{(line, index) => <text fg={index() === 0 ? theme.text.muted : theme.text.base}>{line}</text>}
</For>
</box>
</Show>
@@ -316,16 +316,16 @@ function RejectPrompt(props: {
}))}
backgroundColor={theme.background.raised.base}
border={["left"]}
borderColor={theme.text.feedback.error.default}
borderColor={theme.text.feedback.error.base}
customBorderChars={SplitBorder.customBorderChars}
>
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
<box flexDirection="row" gap={1} paddingLeft={1}>
<text fg={theme.text.feedback.error.default}>{"△"}</text>
<text fg={theme.text.default}>Reject permission</text>
<text fg={theme.text.feedback.error.base}>{"△"}</text>
<text fg={theme.text.base}>Reject permission</text>
</box>
<box paddingLeft={1}>
<text fg={theme.text.subdued}>Tell OpenCode what to do differently</text>
<text fg={theme.text.muted}>Tell OpenCode what to do differently</text>
</box>
</box>
<box
@@ -354,9 +354,9 @@ function RejectPrompt(props: {
val.traits = { status: "REJECT" }
}}
focused={enabled()}
textColor={theme.text.default}
focusedTextColor={theme.text.default}
cursorColor={theme.text.default}
textColor={theme.text.base}
focusedTextColor={theme.text.base}
cursorColor={theme.text.base}
cursorStyle={config.cursor}
/>
<box
@@ -380,8 +380,8 @@ function RejectPrompt(props: {
}))}
onMouseUp={() => props.onConfirm(input.plainText)}
>
<text fg={theme.text.default}>
enter <span style={{ fg: theme.text.subdued }}>confirm</span>
<text fg={theme.text.base}>
enter <span style={{ fg: theme.text.muted }}>confirm</span>
</text>
</box>
<box
@@ -394,8 +394,8 @@ function RejectPrompt(props: {
}))}
onMouseUp={props.onCancel}
>
<text fg={theme.text.default}>
esc <span style={{ fg: theme.text.subdued }}>cancel</span>
<text fg={theme.text.base}>
esc <span style={{ fg: theme.text.muted }}>cancel</span>
</text>
</box>
</box>
@@ -527,8 +527,8 @@ export function SessionQuestion<const T extends Record<string, string>>(props: {
when={props.header}
fallback={
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
<text fg={theme.text.feedback.warning.default}>{"△"}</text>
<text fg={theme.text.default}>{props.title}</text>
<text fg={theme.text.feedback.warning.base}>{"△"}</text>
<text fg={theme.text.base}>{props.title}</text>
</box>
}
>
@@ -578,7 +578,7 @@ export function SessionQuestion<const T extends Record<string, string>>(props: {
backgroundColor={
option === store.selected
? theme.background.action.primary.focused
: theme.background.action.primary.default
: theme.background.action.primary.base
}
onMouseMove={() => setStore("selected", option)}
onMouseUp={() => {
@@ -587,7 +587,7 @@ export function SessionQuestion<const T extends Record<string, string>>(props: {
}}
>
<text
fg={option === store.selected ? theme.text.action.primary.focused : theme.text.action.primary.default}
fg={option === store.selected ? theme.text.action.primary.focused : theme.text.action.primary.base}
>
{props.options[option]}
</text>
@@ -597,17 +597,17 @@ export function SessionQuestion<const T extends Record<string, string>>(props: {
</box>
<box flexDirection="row" gap={2} flexShrink={0}>
<Show when={props.fullscreen}>
<text fg={theme.text.default}>
{shortcuts.get("permission.prompt.fullscreen")} <span style={{ fg: theme.text.subdued }}>{hint()}</span>
<text fg={theme.text.base}>
{shortcuts.get("permission.prompt.fullscreen")} <span style={{ fg: theme.text.muted }}>{hint()}</span>
</text>
</Show>
<Show when={keys.length > 1}>
<text fg={theme.text.default}>
{"⇆"} <span style={{ fg: theme.text.subdued }}>select</span>
<text fg={theme.text.base}>
{"⇆"} <span style={{ fg: theme.text.muted }}>select</span>
</text>
</Show>
<text fg={theme.text.default}>
enter <span style={{ fg: theme.text.subdued }}>confirm</span>
<text fg={theme.text.base}>
enter <span style={{ fg: theme.text.muted }}>confirm</span>
</text>
</box>
</box>
+2 -2
View File
@@ -31,7 +31,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
>
<box flexShrink={0} paddingRight={2} paddingBottom={1}>
<title_shimmer
fg={theme.text.default}
fg={theme.text.base}
rename={{
pending: data.session.title.pending(props.sessionID),
title: withTimestampedFallback(session()),
@@ -62,7 +62,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
height: "100%",
trackOptions: {
backgroundColor: theme.background.raised.base,
foregroundColor: theme.scrollbar.default,
foregroundColor: theme.scrollbar.base,
},
}}
>
@@ -0,0 +1,389 @@
{
"$schema": "https://opencode.ai/theme.json",
"base": {
"categorical": [
"purple",
"orange",
"green",
"blue",
"red"
],
"text": {
"base": "$hue.neutral.200",
"muted": "$hue.neutral.400",
"action": {
"primary": {
"base": "$text.base",
"$disabled": "$hue.neutral.400",
"$focused": "$hue.neutral.900",
"$selected": "$hue.interactive.200"
},
"secondary": {
"base": "$text.muted",
"$hovered": "$text.base"
},
"destructive": {
"base": "$hue.neutral.900",
"$disabled": "$hue.neutral.400"
}
},
"formfield": {
"base": "$hue.neutral.200",
"$hovered": "$hue.interactive.200",
"$focused": "$hue.interactive.200",
"$pressed": "$hue.interactive.200",
"$disabled": "$hue.neutral.400",
"$selected": "$hue.interactive.200"
},
"feedback": {
"error": {
"base": "$hue.red.200"
},
"warning": {
"base": "$hue.accent.200"
},
"success": {
"base": "$hue.green.200"
},
"info": {
"base": "$hue.cyan.200"
}
}
},
"background": {
"base": "$hue.neutral.800",
"raised": {
"base": "$hue.neutral.700",
"high": "$hue.neutral.600",
"max": "$hue.neutral.500"
},
"action": {
"primary": {
"base": "transparent",
"$hovered": "$hue.neutral.700",
"$focused": "$hue.interactive.200",
"$selected": "transparent"
},
"secondary": {
"base": "transparent"
},
"destructive": {
"base": "$hue.red.200"
}
},
"formfield": {
"base": "$background.base"
},
"feedback": {
"error": {
"base": "$background.base"
},
"warning": {
"base": "$background.base"
},
"success": {
"base": "$background.base"
},
"info": {
"base": "$background.base"
}
}
},
"border": {
"base": "#b8b8b8"
},
"scrollbar": {
"base": "#a0a0a0"
},
"diff": {
"text": {
"added": "#1e725c",
"removed": "#c53b53",
"context": "#7086b5",
"hunkHeader": "#7086b5"
},
"background": {
"added": "#d5e5d5",
"removed": "#f7d8db",
"context": "$hue.neutral.700"
},
"highlight": {
"added": "#4db380",
"removed": "#f52a65"
},
"lineNumber": {
"text": "#595959",
"background": {
"added": "#c5d5c5",
"removed": "#e7c8cb"
}
}
},
"syntax": {
"comment": "$hue.neutral.400",
"keyword": "$hue.accent.200",
"function": "$hue.interactive.200",
"variable": "$hue.red.200",
"string": "$hue.green.200",
"number": "$hue.accent.200",
"type": "#b0851f",
"operator": "$hue.cyan.200",
"punctuation": "$hue.neutral.200"
},
"markdown": {
"text": "$hue.neutral.200",
"heading": "$hue.accent.200",
"link": "$hue.interactive.200",
"linkText": "$hue.cyan.200",
"code": "$hue.green.200",
"blockQuote": "#b0851f",
"emphasis": "#b0851f",
"strong": "$hue.accent.200",
"horizontalRule": "$hue.neutral.400",
"listItem": "$hue.interactive.200",
"listEnumeration": "$hue.cyan.200",
"image": "$hue.interactive.200",
"imageText": "$hue.cyan.200",
"codeBlock": "$hue.neutral.200"
},
"@dialog": {
"background": {
"base": "$background.raised.base",
"action": {
"primary": {
"$hovered": "$background.raised.high"
}
}
}
}
},
"light": {
"hue": {
"gray": {
"100": "#000000",
"200": "#1a1a1a",
"300": "#4e4e4e",
"400": "#8a8a8a",
"500": "#bebebe",
"600": "#f5f5f5",
"700": "#fafafa",
"800": "#ffffff",
"900": "#ffffff"
},
"red": {
"100": "#c20f26",
"200": "#d1383d",
"300": "#e05352",
"400": "#ee6b68",
"500": "#fc837d",
"600": "#fda19b",
"700": "#fcbeb9",
"800": "#fed8d5",
"900": "#fff2f0"
},
"orange": {
"100": "#c7811e",
"200": "#d68c27",
"300": "#df9a44",
"400": "#e8a85c",
"500": "#f1b671",
"600": "#fac386",
"700": "#fcd4a8",
"800": "#fee3c7",
"900": "#fef3e7"
},
"yellow": "$hue.gray",
"green": {
"100": "#1f8c44",
"200": "#3d9a57",
"300": "#55a96a",
"400": "#6cb77c",
"500": "#81c68f",
"600": "#96d5a3",
"700": "#ace4b6",
"800": "#c1f3ca",
"900": "#e1fee6"
},
"cyan": {
"100": "#077786",
"200": "#318795",
"300": "#4c97a4",
"400": "#64a8b4",
"500": "#7bb8c4",
"600": "#92c9d4",
"700": "#a8dbe4",
"800": "#bfecf4",
"900": "#e3fafe"
},
"blue": {
"100": "#226bcc",
"200": "#3b7dd8",
"300": "#518ee4",
"400": "#67a0f0",
"500": "#7db1fc",
"600": "#9cc3fa",
"700": "#b7d4fd",
"800": "#d4e5fd",
"900": "#f0f6fe"
},
"purple": {
"100": "#6b47a7",
"200": "#7b5bb6",
"300": "#8c6fc5",
"400": "#9d83d4",
"500": "#af98e3",
"600": "#c1acf2",
"700": "#d3c3fc",
"800": "#e4dbfd",
"900": "#f6f3ff"
},
"accent": "$hue.orange",
"interactive": "$hue.blue",
"neutral": "$hue.gray"
}
},
"dark": {
"hue": {
"gray": {
"100": "#ffffff",
"200": "#eeeeee",
"300": "#b5b5b5",
"400": "#808080",
"500": "#4c4c4c",
"600": "#1e1e1e",
"700": "#141414",
"800": "#0a0a0a",
"900": "#030303"
},
"red": {
"100": "#fd7e87",
"200": "#e06c75",
"300": "#c35a63",
"400": "#a74952",
"500": "#8c3941",
"600": "#722931",
"700": "#591921",
"800": "#410a13",
"900": "#280207"
},
"orange": {
"100": "#fddac5",
"200": "#fab283",
"300": "#d8976c",
"400": "#b67c56",
"500": "#966341",
"600": "#774a2c",
"700": "#593319",
"800": "#3d1d06",
"900": "#1f0b01"
},
"yellow": "$hue.gray",
"green": {
"100": "#96f7a7",
"200": "#7fd88f",
"300": "#68b977",
"400": "#539c61",
"500": "#3d7f4b",
"600": "#296336",
"700": "#144922",
"800": "#00300f",
"900": "#011705"
},
"cyan": {
"100": "#68d0dd",
"200": "#56b6c2",
"300": "#449da7",
"400": "#33848e",
"500": "#226c75",
"600": "#0f555d",
"700": "#093e44",
"800": "#02292e",
"900": "#001518"
},
"blue": {
"100": "#82b4fb",
"200": "#5c9cf5",
"300": "#4c86d6",
"400": "#3c70b8",
"500": "#2c5b9b",
"600": "#1d477f",
"700": "#0f3364",
"800": "#02214a",
"900": "#01112b"
},
"purple": {
"100": "#b38ff4",
"200": "#9d7cd8",
"300": "#8869bd",
"400": "#7357a2",
"500": "#5f4688",
"600": "#4b356f",
"700": "#392557",
"800": "#271640",
"900": "#17072b"
},
"accent": "$hue.purple",
"interactive": "$hue.orange",
"neutral": "$hue.gray"
},
"categorical": [
"blue",
"purple",
"green",
"orange",
"red"
],
"text": {
"action": {
"primary": {
"$focused": "$hue.neutral.800"
},
"destructive": {
"base": "$hue.neutral.800"
}
},
"feedback": {
"warning": {
"base": "#f5a742"
}
}
},
"border": {
"base": "#484848"
},
"scrollbar": {
"base": "#606060"
},
"diff": {
"text": {
"added": "#4fd6be",
"context": "#828bb8",
"hunkHeader": "#828bb8"
},
"background": {
"added": "#20303b",
"removed": "#37222c"
},
"highlight": {
"added": "#b8db87",
"removed": "#e26a75"
},
"lineNumber": {
"text": "#8f8f8f",
"background": {
"added": "#1b2b34",
"removed": "#2d1f26"
}
}
},
"syntax": {
"number": "#f5a742",
"type": "#e5c07b"
},
"markdown": {
"blockQuote": "#e5c07b",
"emphasis": "#e5c07b",
"strong": "#f5a742"
}
}
}
+16 -13
View File
@@ -1,7 +1,8 @@
import { Schema } from "effect"
import { migrateV1, resolveThemeDocument, ThemeDocument, themeDecodeError } from "@opencode/theme/tui"
import { migrateV1, resolveThemeDocument, ThemeDocument, themeDecodeError, type ModeDefinition } from "@opencode/theme/tui"
import { resolveThemeColors } from "./resolve"
import { DEFAULT_THEMES, type Theme, type ThemeV1Json } from "./v1"
import opencode from "./assets/v2/opencode.json" with { type: "json" }
export { DEFAULT_THEMES, generateSyntax, selectedForeground, type Theme, type ThemeV1Json } from "./v1"
export { resolveThemeDocument, type ThemeDocument }
@@ -14,11 +15,23 @@ let systemTheme: ThemeDocumentSource | undefined
const listeners = new Set<(themes: Record<string, ThemeDocumentSource>) => void>()
const parsed = new WeakMap<object, ThemeDocument>()
const decodeThemeDocument = Schema.decodeUnknownSync(ThemeDocument, { reportInput: true })
let opencodeTheme: (ThemeDocument & {
readonly light: ModeDefinition
readonly dark: ModeDefinition
}) | undefined
export function getOpenCodeTheme() {
if (opencodeTheme) return opencodeTheme
const decoded = decodeThemeDocument(opencode) as NonNullable<typeof opencodeTheme>
opencodeTheme = decoded
return decoded
}
function listThemes(): Record<string, ThemeDocumentSource> {
// Priority: defaults < plugin installs < custom files < generated system.
const themes: Record<string, ThemeDocumentSource> = {
...DEFAULT_THEMES,
opencode: getOpenCodeTheme(),
...pluginThemes,
...customThemes,
}
@@ -39,20 +52,14 @@ export function allThemes() {
export function isThemeSource(source: unknown): source is ThemeDocumentSource {
if (typeof source !== "object" || source === null || Array.isArray(source)) return false
return "theme" in source || "version" in source
return "theme" in source || "base" in source
}
export function parseTheme(source: ThemeDocumentSource, name = "theme") {
const cached = parsed.get(source)
if (cached) return cached
const version = source.version ?? 1
const document =
version === 1
? migrateV1(source as ThemeV1Json)
: version === 2
? decodeV2Theme(source, name)
: unsupportedThemeVersion(version)
const document = "theme" in source ? migrateV1(source as ThemeV1Json) : decodeV2Theme(source, name)
parsed.set(source, document)
return document
@@ -117,7 +124,3 @@ function decodeV2Theme(source: ThemeDocumentSource, name: string) {
throw themeDecodeError(error, name)
}
}
function unsupportedThemeVersion(version: unknown): never {
throw new Error(`Unsupported theme version: ${String(version)}`)
}
-2
View File
@@ -22,7 +22,6 @@ import monokai from "./assets/monokai.json" with { type: "json" }
import nightowl from "./assets/nightowl.json" with { type: "json" }
import nord from "./assets/nord.json" with { type: "json" }
import onedark from "./assets/one-dark.json" with { type: "json" }
import opencode from "./assets/opencode.json" with { type: "json" }
import orng from "./assets/orng.json" with { type: "json" }
import osakaJade from "./assets/osaka-jade.json" with { type: "json" }
import palenight from "./assets/palenight.json" with { type: "json" }
@@ -58,7 +57,6 @@ export const DEFAULT_THEMES: Record<string, ThemeV1Json> = {
nord,
["one-dark"]: onedark,
["osaka-jade"]: osakaJade,
opencode,
orng,
["lucent-orng"]: lucentOrng,
palenight,
+3 -3
View File
@@ -30,15 +30,15 @@ export function DialogAlert(props: DialogAlertProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{props.title}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingBottom={1}>
<text fg={theme.text.subdued}>{props.message}</text>
<text fg={theme.text.muted}>{props.message}</text>
</box>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<box
+4 -4
View File
@@ -58,15 +58,15 @@ export function DialogConfirm(props: DialogConfirmProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{props.title}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingBottom={1}>
<text fg={theme.text.subdued}>{props.message}</text>
<text fg={theme.text.muted}>{props.message}</text>
</box>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<For each={["cancel", "confirm"] as const}>
@@ -81,7 +81,7 @@ export function DialogConfirm(props: DialogConfirmProps) {
dialog.clear()
}}
>
<text fg={key === store.active ? theme.text.action.primary.focused : theme.text.subdued}>
<text fg={key === store.active ? theme.text.action.primary.focused : theme.text.muted}>
{Locale.titlecase(props.label?.[key] ?? key)}
</text>
</box>
+17 -17
View File
@@ -85,15 +85,15 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
Export session
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box flexDirection="row" gap={1}>
<text fg={theme.text.default}>Export as:</text>
<text fg={theme.text.base}>Export as:</text>
<box flexDirection="row" gap={1}>
<For each={["markdown", "json"] as const}>
{(format) => (
@@ -105,7 +105,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.background.formfield.focused
: store.format === format
? theme.background.formfield.selected
: theme.background.formfield.default
: theme.background.formfield.base
}
onMouseUp={() => selectFormat(format)}
>
@@ -115,7 +115,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.format === format
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
{store.format === format ? "◉" : "○"} {format === "markdown" ? "Markdown" : "JSON"}
@@ -134,7 +134,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.background.formfield.focused
: store.thinking
? theme.background.formfield.selected
: theme.background.formfield.default
: theme.background.formfield.base
}
onMouseUp={() => {
setStore("active", "thinking")
@@ -147,7 +147,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.thinking
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
{store.thinking ? "[x]" : "[ ]"}
@@ -158,7 +158,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.thinking
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
Include thinking
@@ -172,7 +172,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.background.formfield.focused
: store.tools
? theme.background.formfield.selected
: theme.background.formfield.default
: theme.background.formfield.base
}
onMouseUp={() => {
setStore("active", "tools")
@@ -185,7 +185,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.tools
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
{store.tools ? "[x]" : "[ ]"}
@@ -196,7 +196,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.tools
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
Include tools
@@ -212,7 +212,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.background.formfield.focused
: store.sanitize
? theme.background.formfield.selected
: theme.background.formfield.default
: theme.background.formfield.base
}
onMouseUp={() => {
setStore("active", "sanitize")
@@ -225,7 +225,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.sanitize
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
{store.sanitize ? "[x]" : "[ ]"}
@@ -236,7 +236,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.sanitize
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
Sanitize sensitive data
@@ -250,7 +250,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
backgroundColor={overlayTheme.background.raised.high}
onMouseUp={() => confirm("copy")}
>
<text fg={overlayTheme.text.default}>Copy</text>
<text fg={overlayTheme.text.base}>Copy</text>
</box>
<box
paddingLeft={4}
@@ -258,11 +258,11 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
backgroundColor={
store.active === "export"
? theme.background.action.primary.focused
: theme.background.action.primary.default
: theme.background.action.primary.base
}
onMouseUp={() => confirm("export")}
>
<text fg={store.active === "export" ? theme.text.action.primary.focused : theme.text.action.primary.default}>
<text fg={store.active === "export" ? theme.text.action.primary.focused : theme.text.action.primary.base}>
Export
</text>
</box>
+3 -3
View File
@@ -27,15 +27,15 @@ export function DialogExportResult(props: { path: string; onClose?: () => void }
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
Session exported
</text>
<text fg={theme.text.subdued} onMouseUp={close}>
<text fg={theme.text.muted} onMouseUp={close}>
esc
</text>
</box>
<box>
<text fg={theme.text.default}>{props.path}</text>
<text fg={theme.text.base}>{props.path}</text>
</box>
<box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}>
<box

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