@vladimirven/openswe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/AGENTS.md +203 -0
  2. package/CLAUDE.md +203 -0
  3. package/README.md +166 -0
  4. package/bun.lock +447 -0
  5. package/bunfig.toml +4 -0
  6. package/package.json +42 -0
  7. package/src/app.tsx +84 -0
  8. package/src/components/App.tsx +526 -0
  9. package/src/components/ConfirmDialog.tsx +88 -0
  10. package/src/components/Footer.tsx +50 -0
  11. package/src/components/HelpModal.tsx +136 -0
  12. package/src/components/IssueSelectorModal.tsx +701 -0
  13. package/src/components/ManualSessionModal.tsx +191 -0
  14. package/src/components/PhaseProgress.tsx +45 -0
  15. package/src/components/Preview.tsx +249 -0
  16. package/src/components/ProviderSwitcherModal.tsx +156 -0
  17. package/src/components/ScrollableText.tsx +120 -0
  18. package/src/components/SessionCard.tsx +60 -0
  19. package/src/components/SessionList.tsx +79 -0
  20. package/src/components/SessionTerminal.tsx +89 -0
  21. package/src/components/StatusBar.tsx +84 -0
  22. package/src/components/ThemeSwitcherModal.tsx +237 -0
  23. package/src/components/index.ts +58 -0
  24. package/src/components/session-utils.ts +337 -0
  25. package/src/components/theme.ts +206 -0
  26. package/src/components/types.ts +215 -0
  27. package/src/config/defaults.ts +44 -0
  28. package/src/config/env.ts +67 -0
  29. package/src/config/global.ts +252 -0
  30. package/src/config/index.ts +171 -0
  31. package/src/config/types.ts +131 -0
  32. package/src/core/.gitkeep +0 -0
  33. package/src/core/index.ts +5 -0
  34. package/src/core/parser.ts +62 -0
  35. package/src/core/process-manager.ts +52 -0
  36. package/src/core/session.ts +423 -0
  37. package/src/core/tmux.ts +206 -0
  38. package/src/git/.gitkeep +0 -0
  39. package/src/git/index.ts +8 -0
  40. package/src/git/repo.ts +443 -0
  41. package/src/git/worktree.ts +317 -0
  42. package/src/github/.gitkeep +0 -0
  43. package/src/github/client.ts +208 -0
  44. package/src/github/index.ts +8 -0
  45. package/src/github/issues.ts +351 -0
  46. package/src/index.ts +369 -0
  47. package/src/prompts/.gitkeep +0 -0
  48. package/src/prompts/index.ts +1 -0
  49. package/src/prompts/swe-system.ts +22 -0
  50. package/src/providers/claude.ts +103 -0
  51. package/src/providers/index.ts +21 -0
  52. package/src/providers/opencode.ts +98 -0
  53. package/src/providers/registry.ts +53 -0
  54. package/src/providers/types.ts +117 -0
  55. package/src/store/buffers.ts +234 -0
  56. package/src/store/db.test.ts +579 -0
  57. package/src/store/db.ts +249 -0
  58. package/src/store/index.ts +101 -0
  59. package/src/store/project.ts +119 -0
  60. package/src/store/schema.sql +71 -0
  61. package/src/store/sessions.ts +454 -0
  62. package/src/store/types.ts +194 -0
  63. package/src/theme/context.tsx +170 -0
  64. package/src/theme/custom.ts +134 -0
  65. package/src/theme/index.ts +58 -0
  66. package/src/theme/loader.ts +264 -0
  67. package/src/theme/themes/aura.json +69 -0
  68. package/src/theme/themes/ayu.json +80 -0
  69. package/src/theme/themes/carbonfox.json +248 -0
  70. package/src/theme/themes/catppuccin-frappe.json +233 -0
  71. package/src/theme/themes/catppuccin-macchiato.json +233 -0
  72. package/src/theme/themes/catppuccin.json +112 -0
  73. package/src/theme/themes/cobalt2.json +228 -0
  74. package/src/theme/themes/cursor.json +249 -0
  75. package/src/theme/themes/dracula.json +219 -0
  76. package/src/theme/themes/everforest.json +241 -0
  77. package/src/theme/themes/flexoki.json +237 -0
  78. package/src/theme/themes/github.json +233 -0
  79. package/src/theme/themes/gruvbox.json +242 -0
  80. package/src/theme/themes/kanagawa.json +77 -0
  81. package/src/theme/themes/lucent-orng.json +237 -0
  82. package/src/theme/themes/material.json +235 -0
  83. package/src/theme/themes/matrix.json +77 -0
  84. package/src/theme/themes/mercury.json +252 -0
  85. package/src/theme/themes/monokai.json +221 -0
  86. package/src/theme/themes/nightowl.json +221 -0
  87. package/src/theme/themes/nord.json +223 -0
  88. package/src/theme/themes/one-dark.json +84 -0
  89. package/src/theme/themes/opencode.json +245 -0
  90. package/src/theme/themes/orng.json +249 -0
  91. package/src/theme/themes/osaka-jade.json +93 -0
  92. package/src/theme/themes/palenight.json +222 -0
  93. package/src/theme/themes/rosepine.json +234 -0
  94. package/src/theme/themes/solarized.json +223 -0
  95. package/src/theme/themes/synthwave84.json +226 -0
  96. package/src/theme/themes/tokyonight.json +243 -0
  97. package/src/theme/themes/vercel.json +245 -0
  98. package/src/theme/themes/vesper.json +218 -0
  99. package/src/theme/themes/zenburn.json +223 -0
  100. package/src/theme/types.ts +225 -0
  101. package/src/types/sql.d.ts +4 -0
  102. package/src/utils/ansi-parser.ts +225 -0
  103. package/src/utils/format.ts +46 -0
  104. package/src/utils/id.ts +15 -0
  105. package/src/utils/logger.ts +112 -0
  106. package/src/utils/prerequisites.ts +118 -0
  107. package/src/utils/shell.ts +9 -0
  108. package/src/wizard/flows.ts +419 -0
  109. package/src/wizard/index.ts +37 -0
  110. package/src/wizard/prompts.ts +190 -0
  111. package/src/workspace/detect.test.ts +51 -0
  112. package/src/workspace/detect.ts +223 -0
  113. package/src/workspace/index.ts +71 -0
  114. package/src/workspace/init.ts +131 -0
  115. package/src/workspace/paths.ts +143 -0
  116. package/src/workspace/project.ts +164 -0
  117. package/tsconfig.json +22 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Theme context for OpenSWE
3
+ *
4
+ * Provides reactive theme access throughout the TUI
5
+ */
6
+
7
+ import {
8
+ createContext,
9
+ useContext,
10
+ createSignal,
11
+ onMount,
12
+ createMemo,
13
+ type JSX,
14
+ type Accessor,
15
+ } from "solid-js"
16
+ import type { ResolvedTheme, ThemeJson, ThemeMode } from "./types"
17
+ import { BUNDLED_THEMES, DEFAULT_THEME, resolveTheme } from "./loader"
18
+ import { loadCustomThemes } from "./custom"
19
+
20
+ // ============================================================================
21
+ // Theme Context Type
22
+ // ============================================================================
23
+
24
+ interface ThemeContextValue {
25
+ /** Current resolved theme colors */
26
+ theme: Accessor<ResolvedTheme>
27
+ /** Current theme name */
28
+ themeName: Accessor<string>
29
+ /** Current theme mode (dark/light) */
30
+ themeMode: Accessor<ThemeMode>
31
+ /** Set the active theme by name */
32
+ setTheme: (name: string) => void
33
+ /** Set the theme mode */
34
+ setThemeMode: (mode: ThemeMode) => void
35
+ /** List of all available theme names */
36
+ availableThemes: Accessor<string[]>
37
+ /** Check if a theme is a custom (user-defined) theme */
38
+ isCustomTheme: (name: string) => boolean
39
+ }
40
+
41
+ // ============================================================================
42
+ // Context Creation
43
+ // ============================================================================
44
+
45
+ const ThemeContext = createContext<ThemeContextValue>()
46
+
47
+ // ============================================================================
48
+ // Theme Provider
49
+ // ============================================================================
50
+
51
+ interface ThemeProviderProps {
52
+ children: JSX.Element
53
+ /** Initial theme name (default: tokyonight) */
54
+ initialTheme?: string
55
+ /** Initial theme mode (default: dark) */
56
+ initialMode?: ThemeMode
57
+ }
58
+
59
+ /**
60
+ * Theme provider component
61
+ *
62
+ * Wraps the application and provides reactive theme access
63
+ */
64
+ export function ThemeProvider(props: ThemeProviderProps): JSX.Element {
65
+ const [themeName, setThemeName] = createSignal(
66
+ props.initialTheme ?? DEFAULT_THEME,
67
+ )
68
+ const [themeMode, setThemeMode] = createSignal<ThemeMode>(
69
+ props.initialMode ?? "dark",
70
+ )
71
+ const [customThemes, setCustomThemes] = createSignal<
72
+ Record<string, ThemeJson>
73
+ >({})
74
+
75
+ // Load custom themes on mount
76
+ onMount(async () => {
77
+ const themes = await loadCustomThemes()
78
+ setCustomThemes(themes)
79
+ })
80
+
81
+ // Combined themes (bundled + custom)
82
+ const allThemes = createMemo(() => ({
83
+ ...BUNDLED_THEMES,
84
+ ...customThemes(),
85
+ }))
86
+
87
+ // Available theme names (sorted)
88
+ const availableThemes = createMemo(() =>
89
+ Object.keys(allThemes()).sort(),
90
+ )
91
+
92
+ // Check if a theme is custom
93
+ const isCustomTheme = (name: string): boolean => {
94
+ return name in customThemes()
95
+ }
96
+
97
+ // Resolved theme (reactive)
98
+ const theme = createMemo(() => {
99
+ const name = themeName()
100
+ const mode = themeMode()
101
+ const themes = allThemes()
102
+
103
+ // Try to get the requested theme, fall back to default
104
+ // DEFAULT_THEME is always present in BUNDLED_THEMES
105
+ const themeJson = themes[name] ?? BUNDLED_THEMES[DEFAULT_THEME]!
106
+ return resolveTheme(themeJson, mode)
107
+ })
108
+
109
+ // Set theme with validation
110
+ const setTheme = (name: string): void => {
111
+ const themes = allThemes()
112
+ if (name in themes) {
113
+ setThemeName(name)
114
+ } else {
115
+ // Fall back to default if theme doesn't exist
116
+ setThemeName(DEFAULT_THEME)
117
+ }
118
+ }
119
+
120
+ const contextValue: ThemeContextValue = {
121
+ theme,
122
+ themeName,
123
+ themeMode,
124
+ setTheme,
125
+ setThemeMode,
126
+ availableThemes,
127
+ isCustomTheme,
128
+ }
129
+
130
+ return (
131
+ <ThemeContext.Provider value={contextValue}>
132
+ {props.children}
133
+ </ThemeContext.Provider>
134
+ )
135
+ }
136
+
137
+ // ============================================================================
138
+ // Hook
139
+ // ============================================================================
140
+
141
+ /**
142
+ * Use the theme context
143
+ *
144
+ * Must be used within a ThemeProvider
145
+ *
146
+ * @returns Theme context value
147
+ * @throws Error if used outside ThemeProvider
148
+ */
149
+ export function useTheme(): ThemeContextValue {
150
+ const context = useContext(ThemeContext)
151
+ if (!context) {
152
+ throw new Error("useTheme must be used within a ThemeProvider")
153
+ }
154
+ return context
155
+ }
156
+
157
+ // ============================================================================
158
+ // Static Theme Access
159
+ // ============================================================================
160
+
161
+ /**
162
+ * Get the default theme (for use outside of reactive context)
163
+ *
164
+ * This is useful for components that need theme colors but can't use
165
+ * the reactive context (e.g., during initialization)
166
+ */
167
+ export function getDefaultTheme(): ResolvedTheme {
168
+ // DEFAULT_THEME is always present in BUNDLED_THEMES
169
+ return resolveTheme(BUNDLED_THEMES[DEFAULT_THEME]!, "dark")
170
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Custom theme loader for OpenSWE
3
+ *
4
+ * Loads user-defined themes from ~/.config/openswe/themes/
5
+ */
6
+
7
+ import { homedir } from "os"
8
+ import path from "path"
9
+ import { Glob } from "bun"
10
+ import type { ThemeJson } from "./types"
11
+ import { logger } from "@/utils/logger"
12
+
13
+ // ============================================================================
14
+ // Custom Theme Directory
15
+ // ============================================================================
16
+
17
+ /** Get the custom themes directory path */
18
+ export function getCustomThemesDir(): string {
19
+ return path.join(homedir(), ".config", "openswe", "themes")
20
+ }
21
+
22
+ // ============================================================================
23
+ // Custom Theme Loader
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Load all custom themes from the user's config directory
28
+ *
29
+ * Themes are loaded from ~/.config/openswe/themes/*.json
30
+ * Invalid themes are skipped with a warning
31
+ *
32
+ * @returns Record of theme name to theme JSON
33
+ */
34
+ export async function loadCustomThemes(): Promise<Record<string, ThemeJson>> {
35
+ const themesDir = getCustomThemesDir()
36
+ const result: Record<string, ThemeJson> = {}
37
+
38
+ try {
39
+ // Check if themes directory exists
40
+ const dirFile = Bun.file(themesDir)
41
+ const dirExists = await dirFile.exists()
42
+
43
+ if (!dirExists) {
44
+ // Directory doesn't exist - that's fine, no custom themes
45
+ return result
46
+ }
47
+
48
+ // Scan for JSON files
49
+ const glob = new Glob("*.json")
50
+
51
+ for await (const file of glob.scan({ cwd: themesDir, absolute: false })) {
52
+ const name = path.basename(file, ".json")
53
+ const filePath = path.join(themesDir, file)
54
+
55
+ try {
56
+ const content = await Bun.file(filePath).json()
57
+
58
+ // Basic validation - must have a theme object
59
+ if (!content.theme || typeof content.theme !== "object") {
60
+ logger.warn(`Invalid custom theme ${name}: missing 'theme' object`)
61
+ continue
62
+ }
63
+
64
+ // Validate required colors exist
65
+ const requiredColors = [
66
+ "primary",
67
+ "secondary",
68
+ "accent",
69
+ "error",
70
+ "warning",
71
+ "success",
72
+ "info",
73
+ "text",
74
+ "textMuted",
75
+ "background",
76
+ "backgroundPanel",
77
+ "backgroundElement",
78
+ "border",
79
+ "borderActive",
80
+ "borderSubtle",
81
+ ]
82
+
83
+ const missingColors = requiredColors.filter(
84
+ (c) => !(c in content.theme),
85
+ )
86
+ if (missingColors.length > 0) {
87
+ logger.warn(
88
+ `Invalid custom theme ${name}: missing colors: ${missingColors.join(", ")}`,
89
+ )
90
+ continue
91
+ }
92
+
93
+ result[name] = content as ThemeJson
94
+ logger.debug(`Loaded custom theme: ${name}`)
95
+ } catch (error) {
96
+ logger.warn(`Failed to load custom theme ${name}:`, error)
97
+ }
98
+ }
99
+ } catch (error) {
100
+ // If we can't read the directory at all, just return empty
101
+ logger.debug("Could not read custom themes directory:", error)
102
+ }
103
+
104
+ return result
105
+ }
106
+
107
+ /**
108
+ * Load a single custom theme by name
109
+ *
110
+ * @param name - Theme name (without .json extension)
111
+ * @returns Theme JSON if found, undefined otherwise
112
+ */
113
+ export async function loadCustomTheme(
114
+ name: string,
115
+ ): Promise<ThemeJson | undefined> {
116
+ const themesDir = getCustomThemesDir()
117
+ const filePath = path.join(themesDir, `${name}.json`)
118
+
119
+ try {
120
+ const file = Bun.file(filePath)
121
+ if (!(await file.exists())) {
122
+ return undefined
123
+ }
124
+
125
+ const content = await file.json()
126
+ if (!content.theme || typeof content.theme !== "object") {
127
+ return undefined
128
+ }
129
+
130
+ return content as ThemeJson
131
+ } catch {
132
+ return undefined
133
+ }
134
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Theme module for OpenSWE
3
+ *
4
+ * Provides a complete theming system with 33+ bundled themes
5
+ * and support for custom user themes.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // In app root
10
+ * import { ThemeProvider } from "@/theme"
11
+ *
12
+ * <ThemeProvider initialTheme="tokyonight">
13
+ * <App />
14
+ * </ThemeProvider>
15
+ *
16
+ * // In components
17
+ * import { useTheme } from "@/theme"
18
+ *
19
+ * const { theme, themeName, setTheme } = useTheme()
20
+ * console.log(theme().primary) // "#7aa2f7"
21
+ * ```
22
+ */
23
+
24
+ // Types
25
+ export type {
26
+ ThemeJson,
27
+ ResolvedTheme,
28
+ ThemeMode,
29
+ BundledThemeName,
30
+ ColorValue,
31
+ ColorVariant,
32
+ HexColor,
33
+ RefName,
34
+ } from "./types"
35
+ export { isHexColor, isColorVariant } from "./types"
36
+
37
+ // Loader
38
+ export {
39
+ BUNDLED_THEMES,
40
+ DEFAULT_THEME,
41
+ getBundledThemeNames,
42
+ resolveTheme,
43
+ getTheme,
44
+ } from "./loader"
45
+
46
+ // Custom themes
47
+ export {
48
+ getCustomThemesDir,
49
+ loadCustomThemes,
50
+ loadCustomTheme,
51
+ } from "./custom"
52
+
53
+ // Context
54
+ export {
55
+ ThemeProvider,
56
+ useTheme,
57
+ getDefaultTheme,
58
+ } from "./context"
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Theme loader for OpenSWE
3
+ *
4
+ * Loads bundled themes and resolves color references
5
+ */
6
+
7
+ import type {
8
+ ThemeJson,
9
+ ResolvedTheme,
10
+ ColorValue,
11
+ ThemeMode,
12
+ HexColor,
13
+ RefName,
14
+ } from "./types"
15
+ import { isHexColor, isColorVariant } from "./types"
16
+
17
+ // ============================================================================
18
+ // Bundled Theme Imports
19
+ // ============================================================================
20
+
21
+ import aura from "./themes/aura.json"
22
+ import ayu from "./themes/ayu.json"
23
+ import carbonfox from "./themes/carbonfox.json"
24
+ import catppuccin from "./themes/catppuccin.json"
25
+ import catppuccinFrappe from "./themes/catppuccin-frappe.json"
26
+ import catppuccinMacchiato from "./themes/catppuccin-macchiato.json"
27
+ import cobalt2 from "./themes/cobalt2.json"
28
+ import cursor from "./themes/cursor.json"
29
+ import dracula from "./themes/dracula.json"
30
+ import everforest from "./themes/everforest.json"
31
+ import flexoki from "./themes/flexoki.json"
32
+ import github from "./themes/github.json"
33
+ import gruvbox from "./themes/gruvbox.json"
34
+ import kanagawa from "./themes/kanagawa.json"
35
+ import lucentOrng from "./themes/lucent-orng.json"
36
+ import material from "./themes/material.json"
37
+ import matrix from "./themes/matrix.json"
38
+ import mercury from "./themes/mercury.json"
39
+ import monokai from "./themes/monokai.json"
40
+ import nightowl from "./themes/nightowl.json"
41
+ import nord from "./themes/nord.json"
42
+ import oneDark from "./themes/one-dark.json"
43
+ import opencode from "./themes/opencode.json"
44
+ import orng from "./themes/orng.json"
45
+ import osakaJade from "./themes/osaka-jade.json"
46
+ import palenight from "./themes/palenight.json"
47
+ import rosepine from "./themes/rosepine.json"
48
+ import solarized from "./themes/solarized.json"
49
+ import synthwave84 from "./themes/synthwave84.json"
50
+ import tokyonight from "./themes/tokyonight.json"
51
+ import vercel from "./themes/vercel.json"
52
+ import vesper from "./themes/vesper.json"
53
+ import zenburn from "./themes/zenburn.json"
54
+
55
+ // ============================================================================
56
+ // Bundled Themes Registry
57
+ // ============================================================================
58
+
59
+ /** All bundled themes, keyed by name */
60
+ export const BUNDLED_THEMES: Record<string, ThemeJson> = {
61
+ aura: aura as ThemeJson,
62
+ ayu: ayu as ThemeJson,
63
+ carbonfox: carbonfox as ThemeJson,
64
+ catppuccin: catppuccin as ThemeJson,
65
+ "catppuccin-frappe": catppuccinFrappe as ThemeJson,
66
+ "catppuccin-macchiato": catppuccinMacchiato as ThemeJson,
67
+ cobalt2: cobalt2 as ThemeJson,
68
+ cursor: cursor as ThemeJson,
69
+ dracula: dracula as ThemeJson,
70
+ everforest: everforest as ThemeJson,
71
+ flexoki: flexoki as ThemeJson,
72
+ github: github as ThemeJson,
73
+ gruvbox: gruvbox as ThemeJson,
74
+ kanagawa: kanagawa as ThemeJson,
75
+ "lucent-orng": lucentOrng as ThemeJson,
76
+ material: material as ThemeJson,
77
+ matrix: matrix as ThemeJson,
78
+ mercury: mercury as ThemeJson,
79
+ monokai: monokai as ThemeJson,
80
+ nightowl: nightowl as ThemeJson,
81
+ nord: nord as ThemeJson,
82
+ "one-dark": oneDark as ThemeJson,
83
+ opencode: opencode as ThemeJson,
84
+ orng: orng as ThemeJson,
85
+ "osaka-jade": osakaJade as ThemeJson,
86
+ palenight: palenight as ThemeJson,
87
+ rosepine: rosepine as ThemeJson,
88
+ solarized: solarized as ThemeJson,
89
+ synthwave84: synthwave84 as ThemeJson,
90
+ tokyonight: tokyonight as ThemeJson,
91
+ vercel: vercel as ThemeJson,
92
+ vesper: vesper as ThemeJson,
93
+ zenburn: zenburn as ThemeJson,
94
+ }
95
+
96
+ /** Default theme name */
97
+ export const DEFAULT_THEME = "tokyonight"
98
+
99
+ /** Get list of all bundled theme names */
100
+ export function getBundledThemeNames(): string[] {
101
+ return Object.keys(BUNDLED_THEMES).sort()
102
+ }
103
+
104
+ // ============================================================================
105
+ // Theme Resolution
106
+ // ============================================================================
107
+
108
+ /**
109
+ * Resolve a color value to a hex string
110
+ *
111
+ * @param color - The color value to resolve
112
+ * @param defs - Color definitions from the theme
113
+ * @param mode - Dark or light mode
114
+ * @param visited - Track visited refs to detect cycles
115
+ * @returns Resolved hex color string
116
+ */
117
+ function resolveColor(
118
+ color: ColorValue,
119
+ defs: Record<string, HexColor | RefName>,
120
+ mode: ThemeMode,
121
+ visited: Set<string> = new Set(),
122
+ ): string {
123
+ // If it's a variant object, extract the appropriate mode value
124
+ if (isColorVariant(color)) {
125
+ return resolveColor(color[mode], defs, mode, visited)
126
+ }
127
+
128
+ // If it's a hex color, return it directly
129
+ if (isHexColor(color)) {
130
+ return color
131
+ }
132
+
133
+ // It's a reference name - look it up in defs
134
+ const refName = color as RefName
135
+
136
+ // Check for circular reference
137
+ if (visited.has(refName)) {
138
+ throw new Error(`Circular color reference detected: ${refName}`)
139
+ }
140
+
141
+ const resolved = defs[refName]
142
+ if (resolved === undefined) {
143
+ throw new Error(`Unknown color reference: ${refName}`)
144
+ }
145
+
146
+ // Add to visited set and recurse
147
+ visited.add(refName)
148
+ return resolveColor(resolved, defs, mode, visited)
149
+ }
150
+
151
+ /**
152
+ * Resolve a theme JSON to a fully resolved theme object
153
+ *
154
+ * @param theme - The theme JSON to resolve
155
+ * @param mode - Dark or light mode (default: dark)
156
+ * @returns Resolved theme with all hex colors
157
+ */
158
+ export function resolveTheme(
159
+ theme: ThemeJson,
160
+ mode: ThemeMode = "dark",
161
+ ): ResolvedTheme {
162
+ const defs = theme.defs ?? {}
163
+ const t = theme.theme
164
+
165
+ // Helper to resolve with fallback
166
+ const resolve = (color: ColorValue | undefined, fallback: string): string => {
167
+ if (color === undefined) return fallback
168
+ try {
169
+ return resolveColor(color, defs, mode)
170
+ } catch {
171
+ return fallback
172
+ }
173
+ }
174
+
175
+ // Resolve core colors (required)
176
+ const primary = resolveColor(t.primary, defs, mode)
177
+ const secondary = resolveColor(t.secondary, defs, mode)
178
+ const accent = resolveColor(t.accent, defs, mode)
179
+ const error = resolveColor(t.error, defs, mode)
180
+ const warning = resolveColor(t.warning, defs, mode)
181
+ const success = resolveColor(t.success, defs, mode)
182
+ const info = resolveColor(t.info, defs, mode)
183
+ const text = resolveColor(t.text, defs, mode)
184
+ const textMuted = resolveColor(t.textMuted, defs, mode)
185
+ const background = resolveColor(t.background, defs, mode)
186
+ const backgroundPanel = resolveColor(t.backgroundPanel, defs, mode)
187
+ const backgroundElement = resolveColor(t.backgroundElement, defs, mode)
188
+ const border = resolveColor(t.border, defs, mode)
189
+ const borderActive = resolveColor(t.borderActive, defs, mode)
190
+ const borderSubtle = resolveColor(t.borderSubtle, defs, mode)
191
+
192
+ return {
193
+ // Core colors
194
+ primary,
195
+ secondary,
196
+ accent,
197
+ error,
198
+ warning,
199
+ success,
200
+ info,
201
+ text,
202
+ textMuted,
203
+ background,
204
+ backgroundPanel,
205
+ backgroundElement,
206
+ border,
207
+ borderActive,
208
+ borderSubtle,
209
+
210
+ // Diff colors (with sensible defaults)
211
+ diffAdded: resolve(t.diffAdded, success),
212
+ diffRemoved: resolve(t.diffRemoved, error),
213
+ diffContext: resolve(t.diffContext, textMuted),
214
+ diffHunkHeader: resolve(t.diffHunkHeader, secondary),
215
+ diffHighlightAdded: resolve(t.diffHighlightAdded, success),
216
+ diffHighlightRemoved: resolve(t.diffHighlightRemoved, error),
217
+ diffAddedBg: resolve(t.diffAddedBg, backgroundPanel),
218
+ diffRemovedBg: resolve(t.diffRemovedBg, backgroundPanel),
219
+ diffContextBg: resolve(t.diffContextBg, backgroundPanel),
220
+ diffLineNumber: resolve(t.diffLineNumber, borderSubtle),
221
+ diffAddedLineNumberBg: resolve(t.diffAddedLineNumberBg, backgroundPanel),
222
+ diffRemovedLineNumberBg: resolve(t.diffRemovedLineNumberBg, backgroundPanel),
223
+
224
+ // Markdown colors (with sensible defaults)
225
+ markdownText: resolve(t.markdownText, text),
226
+ markdownHeading: resolve(t.markdownHeading, secondary),
227
+ markdownLink: resolve(t.markdownLink, primary),
228
+ markdownLinkText: resolve(t.markdownLinkText, info),
229
+ markdownCode: resolve(t.markdownCode, success),
230
+ markdownBlockQuote: resolve(t.markdownBlockQuote, warning),
231
+ markdownEmph: resolve(t.markdownEmph, warning),
232
+ markdownStrong: resolve(t.markdownStrong, accent),
233
+ markdownHorizontalRule: resolve(t.markdownHorizontalRule, textMuted),
234
+ markdownListItem: resolve(t.markdownListItem, primary),
235
+ markdownListEnumeration: resolve(t.markdownListEnumeration, info),
236
+ markdownImage: resolve(t.markdownImage, primary),
237
+ markdownImageText: resolve(t.markdownImageText, info),
238
+ markdownCodeBlock: resolve(t.markdownCodeBlock, text),
239
+
240
+ // Syntax highlighting (with sensible defaults)
241
+ syntaxComment: resolve(t.syntaxComment, textMuted),
242
+ syntaxKeyword: resolve(t.syntaxKeyword, secondary),
243
+ syntaxFunction: resolve(t.syntaxFunction, primary),
244
+ syntaxVariable: resolve(t.syntaxVariable, error),
245
+ syntaxString: resolve(t.syntaxString, success),
246
+ syntaxNumber: resolve(t.syntaxNumber, accent),
247
+ syntaxType: resolve(t.syntaxType, warning),
248
+ syntaxOperator: resolve(t.syntaxOperator, info),
249
+ syntaxPunctuation: resolve(t.syntaxPunctuation, text),
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Get a resolved theme by name
255
+ *
256
+ * @param name - Theme name (must be a bundled theme name)
257
+ * @param mode - Dark or light mode (default: dark)
258
+ * @returns Resolved theme, or default theme if not found
259
+ */
260
+ export function getTheme(name: string, mode: ThemeMode = "dark"): ResolvedTheme {
261
+ // DEFAULT_THEME is always present in BUNDLED_THEMES
262
+ const theme = BUNDLED_THEMES[name] ?? BUNDLED_THEMES[DEFAULT_THEME]!
263
+ return resolveTheme(theme, mode)
264
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "$schema": "https://opencode.ai/theme.json",
3
+ "defs": {
4
+ "darkBg": "#0f0f0f",
5
+ "darkBgPanel": "#15141b",
6
+ "darkBorder": "#2d2d2d",
7
+ "darkFgMuted": "#6d6d6d",
8
+ "darkFg": "#edecee",
9
+ "purple": "#a277ff",
10
+ "pink": "#f694ff",
11
+ "blue": "#82e2ff",
12
+ "red": "#ff6767",
13
+ "orange": "#ffca85",
14
+ "cyan": "#61ffca",
15
+ "green": "#9dff65"
16
+ },
17
+ "theme": {
18
+ "primary": "purple",
19
+ "secondary": "pink",
20
+ "accent": "purple",
21
+ "error": "red",
22
+ "warning": "orange",
23
+ "success": "cyan",
24
+ "info": "purple",
25
+ "text": "darkFg",
26
+ "textMuted": "darkFgMuted",
27
+ "background": "darkBg",
28
+ "backgroundPanel": "darkBgPanel",
29
+ "backgroundElement": "darkBgPanel",
30
+ "border": "darkBorder",
31
+ "borderActive": "darkFgMuted",
32
+ "borderSubtle": "darkBorder",
33
+ "diffAdded": "cyan",
34
+ "diffRemoved": "red",
35
+ "diffContext": "darkFgMuted",
36
+ "diffHunkHeader": "darkFgMuted",
37
+ "diffHighlightAdded": "cyan",
38
+ "diffHighlightRemoved": "red",
39
+ "diffAddedBg": "#354933",
40
+ "diffRemovedBg": "#3f191a",
41
+ "diffContextBg": "darkBgPanel",
42
+ "diffLineNumber": "darkBorder",
43
+ "diffAddedLineNumberBg": "#162620",
44
+ "diffRemovedLineNumberBg": "#26161a",
45
+ "markdownText": "darkFg",
46
+ "markdownHeading": "purple",
47
+ "markdownLink": "pink",
48
+ "markdownLinkText": "purple",
49
+ "markdownCode": "cyan",
50
+ "markdownBlockQuote": "darkFgMuted",
51
+ "markdownEmph": "orange",
52
+ "markdownStrong": "purple",
53
+ "markdownHorizontalRule": "darkFgMuted",
54
+ "markdownListItem": "purple",
55
+ "markdownListEnumeration": "purple",
56
+ "markdownImage": "pink",
57
+ "markdownImageText": "purple",
58
+ "markdownCodeBlock": "darkFg",
59
+ "syntaxComment": "darkFgMuted",
60
+ "syntaxKeyword": "pink",
61
+ "syntaxFunction": "purple",
62
+ "syntaxVariable": "purple",
63
+ "syntaxString": "cyan",
64
+ "syntaxNumber": "green",
65
+ "syntaxType": "purple",
66
+ "syntaxOperator": "pink",
67
+ "syntaxPunctuation": "darkFg"
68
+ }
69
+ }