@brimveyn/aimux-config 0.5.1 → 0.5.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux-config",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "TypeScript configuration API for aimux — keymaps, themes, and backends with a fluent builder.",
5
5
  "keywords": [
6
6
  "aimux",
package/src/defaults.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AutoCommitConfig, ResolvedKeymapConfig } from './types'
1
+ import type { AutoCommitConfig, MultiRepoConfig, ResolvedKeymapConfig } from './types'
2
2
 
3
3
  import * as actions from './actions'
4
4
  import { KeymapBuilder } from './keymap-builder'
@@ -12,6 +12,11 @@ export const DEFAULT_AUTO_COMMIT_CONFIG: AutoCommitConfig = {
12
12
  timeoutMs: 60_000,
13
13
  }
14
14
 
15
+ export const DEFAULT_MULTI_REPO_CONFIG: MultiRepoConfig = {
16
+ enabled: true,
17
+ maxDepth: 1,
18
+ }
19
+
15
20
  /**
16
21
  * Build the default keymap — 1:1 transcription of all existing handlers.
17
22
  */
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export { OPENCODE_THEME_IDS, OPENCODE_THEMES } from './themes/opencode'
8
8
  export {
9
9
  accent,
10
10
  border,
11
+ computeSurfaces,
11
12
  diffAddBg,
12
13
  diffDeleteBg,
13
14
  elevated,
@@ -18,11 +19,14 @@ export {
18
19
  muted,
19
20
  selected,
20
21
  } from './palette-utils'
22
+ export type { ResolvedSurfaces } from './palette-utils'
21
23
  export { paletteToShikiTheme } from './palette-to-shiki'
22
24
  export { GroupBuilder, KeymapBuilder, ModeBindingBuilder } from './keymap-builder'
23
25
  export { getDefaultKeymapConfig } from './defaults'
24
26
  export { resolveConfig } from './resolver'
25
27
  export { isAutoCommitEnabled, setAutoCommitEnabled } from './auto-commit-runtime'
28
+ export { getMultiRepoConfig, setMultiRepoConfig } from './multi-repo-runtime'
29
+ export { DEFAULT_MULTI_REPO_CONFIG } from './defaults'
26
30
 
27
31
  // Type exports
28
32
  export type {
@@ -41,6 +45,7 @@ export type {
41
45
  AutoCommitConfig,
42
46
  BackendConfig,
43
47
  BindingDef,
48
+ DiscoveredRepo,
44
49
  FocusMode,
45
50
  GitFileListMode,
46
51
  GitPaneConfig,
@@ -63,6 +68,8 @@ export type {
63
68
  // Mode / state
64
69
  ModeId,
65
70
  ModeKeymapDef,
71
+ MultiRepoConfig,
72
+ MultiRepoState,
66
73
  // Resolved config (internal, but exported for tooling)
67
74
  ResolvedConfig,
68
75
  ResolvedKeymapConfig,
@@ -0,0 +1,20 @@
1
+ import type { MultiRepoConfig } from './types'
2
+
3
+ import { DEFAULT_MULTI_REPO_CONFIG } from './defaults'
4
+
5
+ /**
6
+ * Module-level mirror of `multiRepo` from the resolved config.
7
+ *
8
+ * Set once at startup via `setMultiRepoConfig(resolvedConfig.multiRepo)`. Read
9
+ * by non-React code paths (discovery, poller merge) that need the flag and depth.
10
+ */
11
+
12
+ let current: MultiRepoConfig = DEFAULT_MULTI_REPO_CONFIG
13
+
14
+ export function setMultiRepoConfig(value: MultiRepoConfig): void {
15
+ current = value
16
+ }
17
+
18
+ export function getMultiRepoConfig(): MultiRepoConfig {
19
+ return current
20
+ }
@@ -0,0 +1,75 @@
1
+ // Port (minimal) of sst/opencode's generateNeutralScale + generateNeutralAlphaScale
2
+ // from packages/ui/src/theme/resolve.ts. Only the `ink`-provided branch of
3
+ // generateNeutralScale is needed — every aimux theme supplies an `ink` seed.
4
+
5
+ import { blend, hexToOklch, mixColors, oklchToHex } from './oklch'
6
+
7
+ function clamp(v: number, min: number, max: number): number {
8
+ return Math.max(min, Math.min(max, v))
9
+ }
10
+
11
+ export type NeutralScale = readonly [
12
+ string,
13
+ string,
14
+ string,
15
+ string,
16
+ string,
17
+ string,
18
+ string,
19
+ string,
20
+ string,
21
+ string,
22
+ string,
23
+ string,
24
+ ]
25
+
26
+ function pickDarkBg(base: { c: number; h: number; l: number }): string {
27
+ const tone = clamp(0.19 + Math.max(0, base.l - 0.12) * 0.33 + base.c * 1.95, 0.17, 0.27)
28
+ return oklchToHex({
29
+ c: base.c * Math.max(0, 1 - tone * 0.12),
30
+ h: base.h,
31
+ l: base.l * (1 - tone),
32
+ })
33
+ }
34
+
35
+ function pickLightBg(base: { c: number; h: number; l: number }): string {
36
+ const tone =
37
+ base.l < 0.82 ? 0.86 : clamp(0.1 + base.c * 3.2 + Math.max(0, 0.95 - base.l) * 0.35, 0.1, 0.28)
38
+ return oklchToHex({
39
+ c: base.c * Math.max(0, 1 - tone),
40
+ h: base.h,
41
+ l: base.l + (1 - base.l) * tone,
42
+ })
43
+ }
44
+
45
+ /**
46
+ * Produce a 12-step neutral scale from a palette seed.
47
+ *
48
+ * Step 0 is the effective app background (`palette.neutral` is treated as a
49
+ * seed, not a direct hex). Step 11 lands close to `ink`. Intermediate steps
50
+ * mix the computed bg toward ink using opencode's fixed lightness stops.
51
+ */
52
+ export function generateNeutralScale(seed: string, isDark: boolean, ink: string): NeutralScale {
53
+ const base = hexToOklch(seed)
54
+ const bg = isDark ? pickDarkBg(base) : pickLightBg(base)
55
+
56
+ const steps = isDark
57
+ ? [0, 0.018, 0.039, 0.064, 0.097, 0.143, 0.212, 0.31, 0.46, 0.649, 0.845, 0.984]
58
+ : [0, 0.022, 0.042, 0.068, 0.102, 0.146, 0.208, 0.296, 0.432, 0.61, 0.81, 0.965]
59
+
60
+ return steps.map((step) => mixColors(bg, ink, step)) as unknown as NeutralScale
61
+ }
62
+
63
+ /**
64
+ * 12-step alpha overlay scale blending ink → bg. Used for surface-base /
65
+ * surface-hover / border-base tokens so they stay consistent with the
66
+ * computed bg above.
67
+ */
68
+ export function generateNeutralAlphaScale(neutral: NeutralScale, isDark: boolean): NeutralScale {
69
+ const alphas = isDark
70
+ ? [0.038, 0.066, 0.1, 0.142, 0.19, 0.252, 0.334, 0.446, 0.58, 0.718, 0.854, 0.985]
71
+ : [0.03, 0.06, 0.1, 0.145, 0.2, 0.265, 0.35, 0.47, 0.61, 0.74, 0.86, 0.97]
72
+ const ink = neutral[11]
73
+ const bg = neutral[0]
74
+ return alphas.map((alpha) => blend(ink, bg, alpha)) as unknown as NeutralScale
75
+ }
package/src/oklch.ts ADDED
@@ -0,0 +1,164 @@
1
+ // OKLCH color helpers. Ported (and trimmed) from sst/opencode:
2
+ // packages/ui/src/theme/color.ts — used by neutral-scale.ts to derive
3
+ // aimux surface tokens from a palette seed.
4
+
5
+ export interface OklchColor {
6
+ l: number
7
+ c: number
8
+ h: number
9
+ }
10
+
11
+ function clamp(v: number, min: number, max: number): number {
12
+ return Math.max(min, Math.min(max, v))
13
+ }
14
+
15
+ function hue(v: number): number {
16
+ return ((v % 360) + 360) % 360
17
+ }
18
+
19
+ export function hexToRgb(hex: string): { r: number; g: number; b: number } {
20
+ const h = hex.replace('#', '')
21
+ const full =
22
+ h.length === 3 || h.length === 4
23
+ ? h
24
+ .split('')
25
+ .map((c) => c + c)
26
+ .join('')
27
+ : h
28
+ const rgb = full.length === 8 ? full.slice(0, 6) : full
29
+ const num = parseInt(rgb, 16)
30
+ return {
31
+ b: (num & 255) / 255,
32
+ g: ((num >> 8) & 255) / 255,
33
+ r: ((num >> 16) & 255) / 255,
34
+ }
35
+ }
36
+
37
+ export function rgbToHex(r: number, g: number, b: number): string {
38
+ const toHex = (v: number): string => {
39
+ const clamped = clamp(v, 0, 1)
40
+ const int = Math.round(clamped * 255)
41
+ return int.toString(16).padStart(2, '0')
42
+ }
43
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`
44
+ }
45
+
46
+ function srgbToLinear(c: number): number {
47
+ if (c <= 0.04045) return c / 12.92
48
+ return Math.pow((c + 0.055) / 1.055, 2.4)
49
+ }
50
+
51
+ function linearToSrgb(c: number): number {
52
+ if (c <= 0.0031308) return c * 12.92
53
+ return 1.055 * Math.pow(c, 1 / 2.4) - 0.055
54
+ }
55
+
56
+ export function rgbToOklch(r: number, g: number, b: number): OklchColor {
57
+ const lr = srgbToLinear(r)
58
+ const lg = srgbToLinear(g)
59
+ const lb = srgbToLinear(b)
60
+
61
+ const lLong = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb
62
+ const mLong = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb
63
+ const sLong = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb
64
+
65
+ const lCbrt = Math.cbrt(lLong)
66
+ const mCbrt = Math.cbrt(mLong)
67
+ const sCbrt = Math.cbrt(sLong)
68
+
69
+ const L = 0.2104542553 * lCbrt + 0.793617785 * mCbrt - 0.0040720468 * sCbrt
70
+ const a = 1.9779984951 * lCbrt - 2.428592205 * mCbrt + 0.4505937099 * sCbrt
71
+ const bOk = 0.0259040371 * lCbrt + 0.7827717662 * mCbrt - 0.808675766 * sCbrt
72
+
73
+ const C = Math.sqrt(a * a + bOk * bOk)
74
+ let H = Math.atan2(bOk, a) * (180 / Math.PI)
75
+ if (H < 0) H += 360
76
+
77
+ return { c: C, h: H, l: L }
78
+ }
79
+
80
+ export function oklchToRgb(ok: OklchColor): { r: number; g: number; b: number } {
81
+ const { c: C, h: H, l: L } = ok
82
+ const a = C * Math.cos((H * Math.PI) / 180)
83
+ const b = C * Math.sin((H * Math.PI) / 180)
84
+
85
+ const lLin = L + 0.3963377774 * a + 0.2158037573 * b
86
+ const mLin = L - 0.1055613458 * a - 0.0638541728 * b
87
+ const sLin = L - 0.0894841775 * a - 1.291485548 * b
88
+
89
+ const l3 = lLin * lLin * lLin
90
+ const m3 = mLin * mLin * mLin
91
+ const s3 = sLin * sLin * sLin
92
+
93
+ const lr = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3
94
+ const lg = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3
95
+ const lb = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3
96
+
97
+ return { b: linearToSrgb(lb), g: linearToSrgb(lg), r: linearToSrgb(lr) }
98
+ }
99
+
100
+ export function hexToOklch(hex: string): OklchColor {
101
+ const { b, g, r } = hexToRgb(hex)
102
+ return rgbToOklch(r, g, b)
103
+ }
104
+
105
+ // Pull an out-of-gamut OKLCH into sRGB by shrinking chroma.
106
+ export function fitOklch(ok: OklchColor): OklchColor {
107
+ const base: OklchColor = {
108
+ c: Math.max(0, ok.c),
109
+ h: hue(ok.h),
110
+ l: clamp(ok.l, 0, 1),
111
+ }
112
+ const rgb = oklchToRgb(base)
113
+ if (rgb.r >= 0 && rgb.r <= 1 && rgb.g >= 0 && rgb.g <= 1 && rgb.b >= 0 && rgb.b <= 1) {
114
+ return base
115
+ }
116
+ let c = base.c
117
+ for (let i = 0; i < 24; i++) {
118
+ c *= 0.9
119
+ const next: OklchColor = { ...base, c }
120
+ const out = oklchToRgb(next)
121
+ if (out.r >= 0 && out.r <= 1 && out.g >= 0 && out.g <= 1 && out.b >= 0 && out.b <= 1) {
122
+ return next
123
+ }
124
+ }
125
+ return { ...base, c: 0 }
126
+ }
127
+
128
+ export function oklchToHex(ok: OklchColor): string {
129
+ const { b, g, r } = oklchToRgb(fitOklch(ok))
130
+ return rgbToHex(r, g, b)
131
+ }
132
+
133
+ /** OKLCH-space mix. t=0 returns `a`, t=1 returns `b`. Uses shortest hue arc. */
134
+ export function mixColors(a: string, b: string, t: number): string {
135
+ const ca = hexToOklch(a)
136
+ const cb = hexToOklch(b)
137
+ const delta = ((((cb.h - ca.h) % 360) + 540) % 360) - 180
138
+ return oklchToHex({
139
+ c: ca.c + (cb.c - ca.c) * t,
140
+ h: ca.h + delta * t,
141
+ l: ca.l + (cb.l - ca.l) * t,
142
+ })
143
+ }
144
+
145
+ /** OKLCH shift: additive L, multiplicative C, additive H (degrees). */
146
+ export function shift(color: string, value: { l?: number; c?: number; h?: number }): string {
147
+ const base = hexToOklch(color)
148
+ return oklchToHex({
149
+ c: base.c * (value.c ?? 1),
150
+ h: base.h + (value.h ?? 0),
151
+ l: base.l + (value.l ?? 0),
152
+ })
153
+ }
154
+
155
+ /** sRGB alpha blend: `color` over `background` at `alpha` opacity. */
156
+ export function blend(color: string, background: string, alpha: number): string {
157
+ const fg = hexToRgb(color)
158
+ const bg = hexToRgb(background)
159
+ return rgbToHex(
160
+ fg.r * alpha + bg.r * (1 - alpha),
161
+ fg.g * alpha + bg.g * (1 - alpha),
162
+ fg.b * alpha + bg.b * (1 - alpha)
163
+ )
164
+ }
@@ -1,4 +1,7 @@
1
- import type { AimuxPalette } from './types'
1
+ import type { AimuxPalette, ThemeMode } from './types'
2
+
3
+ import { generateNeutralAlphaScale, generateNeutralScale } from './neutral-scale'
4
+ import { blend } from './oklch'
2
5
 
3
6
  function stripAlpha(hex: string): string {
4
7
  if (hex.length === 9) return hex.slice(0, 7)
@@ -90,3 +93,38 @@ export function extendPalette(
90
93
  if (!overrides) return base
91
94
  return { ...base, ...overrides }
92
95
  }
96
+
97
+ /**
98
+ * Resolved surface colors derived from the OKLCH neutral scale. Prefer this
99
+ * over the standalone `muted`/`faint`/`elevated`/… helpers for runtime UI —
100
+ * those still exist for syntax-theme generation and callers that can't
101
+ * provide a mode.
102
+ */
103
+ export interface ResolvedSurfaces {
104
+ bg: string
105
+ border: string
106
+ elevated: string
107
+ faint: string
108
+ hover: string
109
+ muted: string
110
+ selected: string
111
+ }
112
+
113
+ /** Compute surface tokens for a palette in a given mode. */
114
+ export function computeSurfaces(palette: AimuxPalette, mode: ThemeMode): ResolvedSurfaces {
115
+ const isDark = mode === 'dark'
116
+ const neutral = generateNeutralScale(palette.neutral, isDark, palette.ink)
117
+ const alpha = generateNeutralAlphaScale(neutral, isDark)
118
+ // `selected` is a subtle primary-tinted surface, not the raw `palette.interactive`
119
+ // seed. In opencode themes `interactive` seeds an OKLCH scale we don't port;
120
+ // using it raw produces eye-bleeding accents (e.g. carbonfox `#0f62fe`).
121
+ return {
122
+ bg: neutral[0],
123
+ border: alpha[6],
124
+ elevated: alpha[1],
125
+ faint: neutral[7],
126
+ hover: alpha[2],
127
+ muted: neutral[8],
128
+ selected: blend(palette.primary, neutral[0], 0.18),
129
+ }
130
+ }
package/src/resolver.ts CHANGED
@@ -3,11 +3,16 @@ import type {
3
3
  AutoCommitConfig,
4
4
  ModeId,
5
5
  ModeKeymapDef,
6
+ MultiRepoConfig,
6
7
  ResolvedConfig,
7
8
  ResolvedKeymapConfig,
8
9
  } from './types'
9
10
 
10
- import { DEFAULT_AUTO_COMMIT_CONFIG, getDefaultKeymapConfig } from './defaults'
11
+ import {
12
+ DEFAULT_AUTO_COMMIT_CONFIG,
13
+ DEFAULT_MULTI_REPO_CONFIG,
14
+ getDefaultKeymapConfig,
15
+ } from './defaults'
11
16
  import { KeymapBuilder } from './keymap-builder'
12
17
 
13
18
  /**
@@ -23,12 +28,18 @@ export function resolveConfig(userConfig: AimuxUserConfig): ResolvedConfig {
23
28
  timeoutMs: userConfig.autoCommit?.timeoutMs ?? DEFAULT_AUTO_COMMIT_CONFIG.timeoutMs,
24
29
  }
25
30
 
31
+ const multiRepo: MultiRepoConfig = {
32
+ enabled: userConfig.multiRepo?.enabled ?? DEFAULT_MULTI_REPO_CONFIG.enabled,
33
+ maxDepth: Math.max(1, userConfig.multiRepo?.maxDepth ?? DEFAULT_MULTI_REPO_CONFIG.maxDepth),
34
+ }
35
+
26
36
  return {
27
37
  autoCommit,
28
38
  backends: userConfig.backends ?? {},
29
39
  gitPane: resolveGitPane(userConfig.gitPane),
30
40
  hooks: userConfig.hooks ?? {},
31
41
  keymaps,
42
+ multiRepo,
32
43
  sessionBar: resolveSessionBar(userConfig.sessionBar),
33
44
  sidebar: userConfig.sidebar ?? {},
34
45
  snippets: userConfig.snippets ?? [],
package/src/types.ts CHANGED
@@ -363,6 +363,17 @@ export interface AutoCommitState {
363
363
  bySession: Record<string, AutoCommitSuggestion>
364
364
  }
365
365
 
366
+ export interface DiscoveredRepo {
367
+ path: string
368
+ name: string
369
+ isRoot: boolean
370
+ }
371
+
372
+ export interface MultiRepoState {
373
+ repos: DiscoveredRepo[]
374
+ prefixes: Record<string, string>
375
+ }
376
+
366
377
  export interface AppState {
367
378
  tabs: TabSession[]
368
379
  activeTabId: string | null
@@ -382,6 +393,7 @@ export interface AppState {
382
393
  gitPanel: GitPanelState
383
394
  gitMode: GitModeState
384
395
  autoCommit: AutoCommitState
396
+ multiRepo: MultiRepoState
385
397
  pendingChords: string[] | null
386
398
  }
387
399
 
@@ -823,6 +835,13 @@ export interface AutoCommitConfig {
823
835
  models: Partial<Record<string, string>>
824
836
  }
825
837
 
838
+ export interface MultiRepoConfig {
839
+ /** When true, scan projectPath for nested git repos and aggregate their status into the git panel. */
840
+ enabled: boolean
841
+ /** How deep to scan below projectPath when discovering sub-repos. 1 = immediate children only. */
842
+ maxDepth: number
843
+ }
844
+
826
845
  export interface AimuxThemeConfig {
827
846
  /** Startup override for light/dark theme family. Reapplied on each launch. */
828
847
  initialMode?: ThemeMode
@@ -855,6 +874,7 @@ export interface AimuxUserConfig {
855
874
  hooks?: HooksConfig
856
875
  snippets?: SnippetDef[]
857
876
  autoCommit?: Partial<AutoCommitConfig>
877
+ multiRepo?: Partial<MultiRepoConfig>
858
878
  statusBar?: StatusBarConfig
859
879
  }
860
880
 
@@ -922,5 +942,6 @@ export interface ResolvedConfig {
922
942
  hooks: HooksConfig
923
943
  snippets: SnippetDef[]
924
944
  autoCommit: AutoCommitConfig
945
+ multiRepo: MultiRepoConfig
925
946
  statusBar: StatusBarConfig
926
947
  }