summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/app.tsx87
-rw-r--r--packages/app/src/components/terminal.tsx67
-rw-r--r--packages/app/src/pages/layout.tsx173
3 files changed, 221 insertions, 106 deletions
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index de8fcf7d1..bf5ba9566 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -8,6 +8,7 @@ import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
import { Diff } from "@opencode-ai/ui/diff"
import { Code } from "@opencode-ai/ui/code"
+import { ThemeProvider } from "@opencode-ai/ui/theme"
import { GlobalSyncProvider } from "@/context/global-sync"
import { LayoutProvider } from "@/context/layout"
import { GlobalSDKProvider } from "@/context/global-sdk"
@@ -45,48 +46,50 @@ export function App() {
return (
<MetaProvider>
<Font />
- <ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
- <DialogProvider>
- <MarkedProvider>
- <DiffComponentProvider component={Diff}>
- <CodeComponentProvider component={Code}>
- <GlobalSDKProvider url={url}>
- <GlobalSyncProvider>
- <LayoutProvider>
- <NotificationProvider>
- <Router
- root={(props) => (
- <CommandProvider>
- <Layout>{props.children}</Layout>
- </CommandProvider>
- )}
- >
- <Route path="/" component={Home} />
- <Route path="/:dir" component={DirectoryLayout}>
- <Route path="/" component={() => <Navigate href="session" />} />
- <Route
- path="/session/:id?"
- component={(p) => (
- <Show when={p.params.id || true} keyed>
- <TerminalProvider>
- <PromptProvider>
- <Session />
- </PromptProvider>
- </TerminalProvider>
- </Show>
- )}
- />
- </Route>
- </Router>
- </NotificationProvider>
- </LayoutProvider>
- </GlobalSyncProvider>
- </GlobalSDKProvider>
- </CodeComponentProvider>
- </DiffComponentProvider>
- </MarkedProvider>
- </DialogProvider>
- </ErrorBoundary>
+ <ThemeProvider>
+ <ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
+ <DialogProvider>
+ <MarkedProvider>
+ <DiffComponentProvider component={Diff}>
+ <CodeComponentProvider component={Code}>
+ <GlobalSDKProvider url={url}>
+ <GlobalSyncProvider>
+ <LayoutProvider>
+ <NotificationProvider>
+ <Router
+ root={(props) => (
+ <CommandProvider>
+ <Layout>{props.children}</Layout>
+ </CommandProvider>
+ )}
+ >
+ <Route path="/" component={Home} />
+ <Route path="/:dir" component={DirectoryLayout}>
+ <Route path="/" component={() => <Navigate href="session" />} />
+ <Route
+ path="/session/:id?"
+ component={(p) => (
+ <Show when={p.params.id || true} keyed>
+ <TerminalProvider>
+ <PromptProvider>
+ <Session />
+ </PromptProvider>
+ </TerminalProvider>
+ </Show>
+ )}
+ />
+ </Route>
+ </Router>
+ </NotificationProvider>
+ </LayoutProvider>
+ </GlobalSyncProvider>
+ </GlobalSDKProvider>
+ </CodeComponentProvider>
+ </DiffComponentProvider>
+ </MarkedProvider>
+ </DialogProvider>
+ </ErrorBoundary>
+ </ThemeProvider>
</MetaProvider>
)
}
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx
index 1d6bab2a4..03251fe5f 100644
--- a/packages/app/src/components/terminal.tsx
+++ b/packages/app/src/components/terminal.tsx
@@ -1,9 +1,9 @@
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
-import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
+import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
import { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/terminal"
-import { usePrefersDark } from "@solid-primitives/media"
+import { resolveThemeVariant, useTheme } from "@opencode-ai/ui/theme"
export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
@@ -12,8 +12,28 @@ export interface TerminalProps extends ComponentProps<"div"> {
onConnectError?: (error: unknown) => void
}
+type TerminalColors = {
+ background: string
+ foreground: string
+ cursor: string
+}
+
+const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
+ light: {
+ background: "#fcfcfc",
+ foreground: "#211e1e",
+ cursor: "#211e1e",
+ },
+ dark: {
+ background: "#191515",
+ foreground: "#d4d4d4",
+ cursor: "#d4d4d4",
+ },
+}
+
export const Terminal = (props: TerminalProps) => {
const sdk = useSDK()
+ const theme = useTheme()
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
let ws: WebSocket
@@ -22,7 +42,35 @@ export const Terminal = (props: TerminalProps) => {
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
let handleResize: () => void
- const prefersDark = usePrefersDark()
+
+ const getTerminalColors = (): TerminalColors => {
+ const mode = theme.mode()
+ const fallback = DEFAULT_TERMINAL_COLORS[mode]
+ const currentTheme = theme.themes()[theme.themeId()]
+ if (!currentTheme) return fallback
+ const variant = mode === "dark" ? currentTheme.dark : currentTheme.light
+ if (!variant?.seeds) return fallback
+ const resolved = resolveThemeVariant(variant, mode === "dark")
+ const text = resolved["text-base"] ?? fallback.foreground
+ const background = resolved["background-stronger"] ?? fallback.background
+ return {
+ background,
+ foreground: text,
+ cursor: text,
+ }
+ }
+
+ const [terminalColors, setTerminalColors] = createSignal<TerminalColors>(getTerminalColors())
+
+ createEffect(() => {
+ const colors = getTerminalColors()
+ setTerminalColors(colors)
+ if (!term) return
+ const setOption = (term as unknown as { setOption?: (key: string, value: TerminalColors) => void }).setOption
+ if (!setOption) return
+ setOption("theme", colors)
+ })
+
const focusTerminal = () => term?.focus()
const copySelection = () => {
if (!term || !term.hasSelection()) return false
@@ -62,17 +110,7 @@ export const Terminal = (props: TerminalProps) => {
fontSize: 14,
fontFamily: "IBM Plex Mono, monospace",
allowTransparency: true,
- theme: prefersDark()
- ? {
- background: "#191515",
- foreground: "#d4d4d4",
- cursor: "#d4d4d4",
- }
- : {
- background: "#fcfcfc",
- foreground: "#211e1e",
- cursor: "#211e1e",
- },
+ theme: terminalColors(),
scrollback: 10_000,
ghostty,
})
@@ -192,6 +230,7 @@ export const Terminal = (props: TerminalProps) => {
ref={container}
data-component="terminal"
data-prevent-autofocus
+ style={{ "background-color": terminalColors().background }}
classList={{
...(local.classList ?? {}),
"size-full px-6 py-3 font-mono": true,
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 0b4e040b7..08b340187 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -47,6 +47,7 @@ import { useNotification } from "@/context/notification"
import { Binary } from "@opencode-ai/util/binary"
import { Header } from "@/components/header"
import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { useCommand } from "@/context/command"
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
@@ -89,6 +90,41 @@ export default function Layout(props: ParentProps) {
const providers = useProviders()
const dialog = useDialog()
const command = useCommand()
+ const theme = useTheme()
+ const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
+ const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
+ const colorSchemeLabel: Record<ColorScheme, string> = {
+ system: "System",
+ light: "Light",
+ dark: "Dark",
+ }
+
+ function cycleTheme(direction = 1) {
+ const ids = availableThemeEntries().map(([id]) => id)
+ if (ids.length === 0) return
+ const currentIndex = ids.indexOf(theme.themeId())
+ const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length
+ const nextThemeId = ids[nextIndex]
+ theme.setTheme(nextThemeId)
+ const nextTheme = theme.themes()[nextThemeId]
+ showToast({
+ title: "Theme switched",
+ description: nextTheme?.name ?? nextThemeId,
+ })
+ }
+
+ function cycleColorScheme(direction = 1) {
+ const current = theme.colorScheme()
+ const currentIndex = colorSchemeOrder.indexOf(current)
+ const nextIndex =
+ currentIndex === -1 ? 0 : (currentIndex + direction + colorSchemeOrder.length) % colorSchemeOrder.length
+ const next = colorSchemeOrder[nextIndex]
+ theme.setColorScheme(next)
+ showToast({
+ title: "Color scheme",
+ description: colorSchemeLabel[next],
+ })
+ }
onMount(async () => {
if (platform.checkUpdate && platform.update && platform.restart) {
@@ -286,57 +322,94 @@ export default function Layout(props: ParentProps) {
}
}
- command.register(() => [
- {
- id: "sidebar.toggle",
- title: "Toggle sidebar",
- category: "View",
- keybind: "mod+b",
- onSelect: () => layout.sidebar.toggle(),
- },
- ...(platform.openDirectoryPickerDialog
- ? [
- {
- id: "project.open",
- title: "Open project",
- category: "Project",
- keybind: "mod+o",
- onSelect: () => chooseProject(),
- },
- ]
- : []),
- {
- id: "provider.connect",
- title: "Connect provider",
- category: "Provider",
- onSelect: () => connectProvider(),
- },
- {
- id: "session.previous",
- title: "Previous session",
- category: "Session",
- keybind: "alt+arrowup",
- onSelect: () => navigateSessionByOffset(-1),
- },
- {
- id: "session.next",
- title: "Next session",
- category: "Session",
- keybind: "alt+arrowdown",
- onSelect: () => navigateSessionByOffset(1),
- },
- {
- id: "session.archive",
- title: "Archive session",
- category: "Session",
- keybind: "mod+shift+backspace",
- disabled: !params.dir || !params.id,
- onSelect: () => {
- const session = currentSessions().find((s) => s.id === params.id)
- if (session) archiveSession(session)
+ command.register(() => {
+ const commands = [
+ {
+ id: "sidebar.toggle",
+ title: "Toggle sidebar",
+ category: "View",
+ keybind: "mod+b",
+ onSelect: () => layout.sidebar.toggle(),
+ },
+ ...(platform.openDirectoryPickerDialog
+ ? [
+ {
+ id: "project.open",
+ title: "Open project",
+ category: "Project",
+ keybind: "mod+o",
+ onSelect: () => chooseProject(),
+ },
+ ]
+ : []),
+ {
+ id: "provider.connect",
+ title: "Connect provider",
+ category: "Provider",
+ onSelect: () => connectProvider(),
+ },
+ {
+ id: "session.previous",
+ title: "Previous session",
+ category: "Session",
+ keybind: "alt+arrowup",
+ onSelect: () => navigateSessionByOffset(-1),
},
- },
- ])
+ {
+ id: "session.next",
+ title: "Next session",
+ category: "Session",
+ keybind: "alt+arrowdown",
+ onSelect: () => navigateSessionByOffset(1),
+ },
+ {
+ id: "session.archive",
+ title: "Archive session",
+ category: "Session",
+ keybind: "mod+shift+backspace",
+ disabled: !params.dir || !params.id,
+ onSelect: () => {
+ const session = currentSessions().find((s) => s.id === params.id)
+ if (session) archiveSession(session)
+ },
+ },
+ {
+ id: "theme.cycle",
+ title: "Cycle theme",
+ category: "Theme",
+ keybind: "mod+shift+t",
+ onSelect: () => cycleTheme(1),
+ },
+ ]
+
+ for (const [id, definition] of availableThemeEntries()) {
+ commands.push({
+ id: `theme.set.${id}`,
+ title: `Use theme: ${definition.name ?? id}`,
+ category: "Theme",
+ onSelect: () => theme.setTheme(id),
+ })
+ }
+
+ commands.push({
+ id: "theme.scheme.cycle",
+ title: "Cycle color scheme",
+ category: "Theme",
+ keybind: "mod+shift+s",
+ onSelect: () => cycleColorScheme(1),
+ })
+
+ for (const scheme of colorSchemeOrder) {
+ commands.push({
+ id: `theme.scheme.${scheme}`,
+ title: `Use color scheme: ${colorSchemeLabel[scheme]}`,
+ category: "Theme",
+ onSelect: () => theme.setColorScheme(scheme),
+ })
+ }
+
+ return commands
+ })
function connectProvider() {
dialog.show(() => <DialogSelectProvider />)