mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-18 23:06:25 +00:00
Compare commits
8
Commits
ci-cache-speed
...
v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
681338c533 | ||
|
|
5464aa4a74 | ||
|
|
36083c9520 | ||
|
|
168aba6183 | ||
|
|
ec19dc09c7 | ||
|
|
fca4701688 | ||
|
|
c720ca20ed | ||
|
|
dae1e8abbd |
@@ -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" } },
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}),
|
||||
})
|
||||
})
|
||||
}),
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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[] = []
|
||||
|
||||
@@ -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 })
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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[]
|
||||
@@ -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
|
||||
@@ -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`]),
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" } },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(""),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)}`)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user