claudeup 6.3.2 → 6.4.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.
- package/package.json +4 -4
- package/src/__tests__/cli-live.test.ts +9 -2
- package/src/__tests__/footer-hints.test.ts +40 -0
- package/src/__tests__/gitignore-prerun.test.ts +6 -13
- package/src/__tests__/hook-import-policy.test.ts +90 -0
- package/src/__tests__/hook-process.test.ts +256 -0
- package/src/__tests__/hook-registration.test.ts +224 -0
- package/src/__tests__/manifest.test.ts +134 -0
- package/src/__tests__/model-visuals.test.tsx +789 -0
- package/src/__tests__/models-adapter.test.ts +317 -0
- package/src/__tests__/models-cli.test.ts +173 -0
- package/src/__tests__/models-core.test.ts +640 -0
- package/src/__tests__/models-manager.test.ts +497 -0
- package/src/__tests__/models-screen-state.test.ts +259 -0
- package/src/__tests__/profile-materializer.test.ts +46 -0
- package/src/__tests__/resolver.test.ts +36 -0
- package/src/__tests__/settings-file.test.ts +179 -0
- package/src/__tests__/symlink-manager.test.ts +65 -1
- package/src/__tests__/tabbar-layout.test.ts +40 -2
- package/src/__tests__/theme-adaptive-colors.test.ts +48 -1
- package/src/cli/doctor.ts +90 -0
- package/src/cli/hook.ts +129 -0
- package/src/cli/models.ts +214 -0
- package/src/cli/router.ts +12 -0
- package/src/data/gitignore-defaults.ts +4 -0
- package/src/data/models-presets.ts +281 -0
- package/src/data/predefined-profiles.ts +9 -0
- package/src/data/settings-catalog.ts +11 -4
- package/src/main.tsx +51 -82
- package/src/services/hook-registration.ts +218 -0
- package/src/services/manifest.ts +84 -0
- package/src/services/models-core.ts +628 -0
- package/src/services/models-manager.ts +606 -0
- package/src/services/profile-materializer.ts +17 -0
- package/src/services/resolver.ts +11 -0
- package/src/services/settings-file.ts +69 -0
- package/src/services/styles-manager.ts +23 -45
- package/src/services/symlink-manager.ts +57 -11
- package/src/tui.tsx +112 -0
- package/src/types/bun.d.ts +21 -0
- package/src/types/index.ts +14 -0
- package/src/ui/App.tsx +15 -3
- package/src/ui/adapters/modelsAdapter.ts +170 -0
- package/src/ui/components/TabBar.tsx +9 -4
- package/src/ui/components/layout/FooterHints.tsx +20 -3
- package/src/ui/components/layout/ScreenLayout.tsx +87 -7
- package/src/ui/components/primitives/MetaText.tsx +27 -1
- package/src/ui/renderers/modelRenderers.tsx +1004 -0
- package/src/ui/renderers/modelVisuals.tsx +853 -0
- package/src/ui/renderers/skillRenderers.tsx +13 -3
- package/src/ui/renderers/styleRenderers.tsx +7 -3
- package/src/ui/screens/ModelsScreen.tsx +478 -0
- package/src/ui/screens/StylesScreen.tsx +8 -13
- package/src/ui/screens/index.ts +1 -0
- package/src/ui/state/reducer.ts +94 -0
- package/src/ui/state/types.ts +65 -2
- package/src/ui/theme-mode.ts +116 -0
- package/src/ui/theme.ts +26 -0
package/src/ui/state/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ModelsConfig, ModelsStatus } from "../../services/models-core.js";
|
|
1
2
|
import type { PluginInfo } from "../../services/plugin-manager.js";
|
|
2
3
|
import type { VersionMismatchInfo } from "../../services/plugin-version-check.js";
|
|
3
4
|
import type {
|
|
@@ -26,7 +27,8 @@ export type Screen =
|
|
|
26
27
|
| "skills"
|
|
27
28
|
| "styles"
|
|
28
29
|
| "gitignore"
|
|
29
|
-
| "alias"
|
|
30
|
+
| "alias"
|
|
31
|
+
| "models";
|
|
30
32
|
|
|
31
33
|
export type Route =
|
|
32
34
|
| { screen: "plugins" }
|
|
@@ -38,7 +40,8 @@ export type Route =
|
|
|
38
40
|
| { screen: "skills" }
|
|
39
41
|
| { screen: "styles" }
|
|
40
42
|
| { screen: "gitignore" }
|
|
41
|
-
| { screen: "alias" }
|
|
43
|
+
| { screen: "alias" }
|
|
44
|
+
| { screen: "models" };
|
|
42
45
|
|
|
43
46
|
// ============================================================================
|
|
44
47
|
// Async Data Types
|
|
@@ -253,6 +256,45 @@ export interface StylesScreenState {
|
|
|
253
256
|
isFilling: boolean;
|
|
254
257
|
}
|
|
255
258
|
|
|
259
|
+
/**
|
|
260
|
+
* What the Models screen reads in one go.
|
|
261
|
+
*
|
|
262
|
+
* Both halves are needed and neither implies the other: `status` says whether
|
|
263
|
+
* the routing is in force, and `config` is the routing itself — which the
|
|
264
|
+
* screen needs in full to give a hand-edited config a row of its own.
|
|
265
|
+
*/
|
|
266
|
+
export interface ModelsSnapshot {
|
|
267
|
+
status: ModelsStatus;
|
|
268
|
+
/** The validated config, or null when there is none (or it failed validation). */
|
|
269
|
+
config: ModelsConfig | null;
|
|
270
|
+
/** Where `.claude/models.json` is, whether or not it exists. */
|
|
271
|
+
path: string;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface ModelsScreenState {
|
|
275
|
+
selectedIndex: number;
|
|
276
|
+
searchQuery: string;
|
|
277
|
+
/** Routing status + the config behind it, in one read. */
|
|
278
|
+
data: AsyncData<ModelsSnapshot>;
|
|
279
|
+
/**
|
|
280
|
+
* The status line's current message, or null for the default line.
|
|
281
|
+
*
|
|
282
|
+
* App state for the same reason the Styles screen's is: `Router` swaps the
|
|
283
|
+
* component type on a tab change, so anything held in the screen's own
|
|
284
|
+
* `useState` is destroyed by `0 → 1 → 0`. The message survives until the next
|
|
285
|
+
* action replaces it, so the last thing you did is still on screen.
|
|
286
|
+
*/
|
|
287
|
+
status: { text: string; tone: "success" | "error" } | null;
|
|
288
|
+
/**
|
|
289
|
+
* True while an apply or a clear is writing.
|
|
290
|
+
*
|
|
291
|
+
* App state, not screen state, because the screen UNMOUNTS on a tab switch.
|
|
292
|
+
* An apply rewrites the profile, re-materializes it and registers a hook —
|
|
293
|
+
* two of those running at once would interleave writes to the same files.
|
|
294
|
+
*/
|
|
295
|
+
isApplying: boolean;
|
|
296
|
+
}
|
|
297
|
+
|
|
256
298
|
// ============================================================================
|
|
257
299
|
// App State
|
|
258
300
|
// ============================================================================
|
|
@@ -287,6 +329,7 @@ export interface AppState {
|
|
|
287
329
|
profiles: ProfilesScreenState;
|
|
288
330
|
skills: SkillsScreenState;
|
|
289
331
|
styles: StylesScreenState;
|
|
332
|
+
models: ModelsScreenState;
|
|
290
333
|
}
|
|
291
334
|
|
|
292
335
|
// ============================================================================
|
|
@@ -412,5 +455,25 @@ export type AppAction =
|
|
|
412
455
|
| { type: "STYLES_FILL_END" }
|
|
413
456
|
| { type: "STYLES_SET_SELECTION"; ids: string[] }
|
|
414
457
|
|
|
458
|
+
// Models screen
|
|
459
|
+
| { type: "MODELS_SELECT"; index: number }
|
|
460
|
+
| { type: "MODELS_SET_SEARCH"; query: string }
|
|
461
|
+
// Computed IN the reducer, from current state, for the same reason the
|
|
462
|
+
// STYLES_SEARCH_* pair is: a screen-side `searchQuery + char` reads a value
|
|
463
|
+
// captured at render time, so keystrokes arriving faster than React
|
|
464
|
+
// re-renders all overwrite each other.
|
|
465
|
+
| { type: "MODELS_SEARCH_APPEND"; char: string }
|
|
466
|
+
| { type: "MODELS_SEARCH_BACKSPACE" }
|
|
467
|
+
| { type: "MODELS_DATA_LOADING" }
|
|
468
|
+
| { type: "MODELS_DATA_SUCCESS"; snapshot: ModelsSnapshot }
|
|
469
|
+
| { type: "MODELS_DATA_ERROR"; error: Error }
|
|
470
|
+
| {
|
|
471
|
+
type: "MODELS_STATUS_SET";
|
|
472
|
+
status: { text: string; tone: "success" | "error" };
|
|
473
|
+
}
|
|
474
|
+
| { type: "MODELS_STATUS_CLEAR" }
|
|
475
|
+
| { type: "MODELS_APPLY_START" }
|
|
476
|
+
| { type: "MODELS_APPLY_END" }
|
|
477
|
+
|
|
415
478
|
// Data refresh - triggers screens to refetch
|
|
416
479
|
| { type: "DATA_REFRESH_COMPLETE" };
|
package/src/ui/theme-mode.ts
CHANGED
|
@@ -101,6 +101,122 @@ export const COMPONENT_BADGE = {
|
|
|
101
101
|
|
|
102
102
|
export type ComponentBadgeKind = keyof typeof COMPONENT_BADGE;
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Badge and chart colours for the four Claude models, plus `inherit`.
|
|
106
|
+
*
|
|
107
|
+
* Three strengths, because a badge and a bar cannot share an ink: twenty characters of a
|
|
108
|
+
* colour weigh far more than six, so what reads as a label reads as a slab when it is a
|
|
109
|
+
* chart. `bg`/`fg` are the chip; `bar` is the distribution bar's fill.
|
|
110
|
+
*
|
|
111
|
+
* ## The two pages are built differently, deliberately
|
|
112
|
+
*
|
|
113
|
+
* `COMPONENT_BADGE` above explains why a chip needs two ends at all. This map goes one step
|
|
114
|
+
* further and does not MIRROR them. Cream already carries the light, so a light chip is a
|
|
115
|
+
* pale wash under mid-tone ink and stays quiet. A near-black page carries none, so the same
|
|
116
|
+
* construction turns grey and the screen goes flat — a symmetric palette was built, shown,
|
|
117
|
+
* and rejected on sight as "dark looks like the light theme". A dark chip is therefore a
|
|
118
|
+
* SATURATED MID-TONE under near-white ink: a wash toward black is dark, a wash toward white
|
|
119
|
+
* is the light theme, and neither of those is a colour.
|
|
120
|
+
*
|
|
121
|
+
* ## The dark fills clear a page we cannot see
|
|
122
|
+
*
|
|
123
|
+
* `CONTRAST_REFERENCE_KEYS.dark` is `#1C1C1E`, but a real terminal may be pure black or a
|
|
124
|
+
* slate like One Dark's `#282C34`. MEASURED, the `COMPONENT_BADGE` dark washes sit at
|
|
125
|
+
* 1.00–1.07:1 against that slate — the same luminance as the page — so the chip loses its
|
|
126
|
+
* edge and reads as a hole punched in the row. These fills clear every ground: 3.1–4.3:1 on
|
|
127
|
+
* black, 2.8–2.9 on slate, and they still clear 4.5:1 on cream, so even a mis-detected theme
|
|
128
|
+
* is merely odd rather than unreadable.
|
|
129
|
+
*
|
|
130
|
+
* Hues stay far apart for the same reason the scope squares' do: one or two characters of
|
|
131
|
+
* colour carry no readable hue SHIFT, only a readable hue DIFFERENCE.
|
|
132
|
+
*/
|
|
133
|
+
export const MODEL_BADGE = {
|
|
134
|
+
opus: {
|
|
135
|
+
light: { bg: "#D3E7EC", fg: "#0E5F73", bar: "#92CEDC" },
|
|
136
|
+
dark: { bg: "#0E7C94", fg: "#F0FDFF", bar: "#48C8E0" },
|
|
137
|
+
},
|
|
138
|
+
sonnet: {
|
|
139
|
+
light: { bg: "#D6EEDD", fg: "#166534", bar: "#93D9B2" },
|
|
140
|
+
dark: { bg: "#12804F", fg: "#EFFFF6", bar: "#4FD98F" },
|
|
141
|
+
},
|
|
142
|
+
haiku: {
|
|
143
|
+
light: { bg: "#E6E6E0", fg: "#4B5563", bar: "#C9CCD2" },
|
|
144
|
+
dark: { bg: "#697180", fg: "#F8FAFC", bar: "#8A92A2" },
|
|
145
|
+
},
|
|
146
|
+
fable: {
|
|
147
|
+
light: { bg: "#E9DCF5", fg: "#6B21A8", bar: "#C3A6EC" },
|
|
148
|
+
dark: { bg: "#7B4CD6", fg: "#F6F0FF", bar: "#A87BE8" },
|
|
149
|
+
},
|
|
150
|
+
/** Not a model — the absence of a routing decision. Grey on both pages, so it never
|
|
151
|
+
* reads as a fifth choice sitting beside four real ones. */
|
|
152
|
+
inherit: {
|
|
153
|
+
light: { bg: "#EFEFE6", fg: "#8A8A80", bar: "#DEDED2" },
|
|
154
|
+
dark: { bg: "#4A4A54", fg: "#D0D2D8", bar: "#4E4E58" },
|
|
155
|
+
},
|
|
156
|
+
} as const;
|
|
157
|
+
|
|
158
|
+
export type ModelBadgeKind = keyof typeof MODEL_BADGE;
|
|
159
|
+
|
|
160
|
+
export interface ModelInk {
|
|
161
|
+
bg: string;
|
|
162
|
+
fg: string;
|
|
163
|
+
bar: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Chip and bar colours for one model, for the resolved mode.
|
|
168
|
+
*
|
|
169
|
+
* Total by construction: a name this build does not know gets `inherit`'s grey, which reads
|
|
170
|
+
* as "claudeup does not recognise this" rather than as a missing colour. A hand-edited
|
|
171
|
+
* config can name anything.
|
|
172
|
+
*/
|
|
173
|
+
export function modelBadge(kind: string): ModelInk {
|
|
174
|
+
const key = (kind in MODEL_BADGE ? kind : "inherit") as ModelBadgeKind;
|
|
175
|
+
return MODEL_BADGE[key][detected];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The UNLIT track: the `·` behind an effort dot, the `░` behind a bar.
|
|
180
|
+
*
|
|
181
|
+
* Light reuses `SCOPE_OFF_FILL`, which already solves this there. Dark does NOT: at
|
|
182
|
+
* `#3A3A40` the track measures 1.51:1 on the reference page and simply vanishes, so a bar
|
|
183
|
+
* reads as a floating block with no scale and the effort dots lose the denominator that
|
|
184
|
+
* makes them a level rather than a count. `#4A4A52` is 1.94:1 — still recessive, still
|
|
185
|
+
* visible.
|
|
186
|
+
*/
|
|
187
|
+
export const TRACK_FILL = {
|
|
188
|
+
light: SCOPE_OFF_FILL.light,
|
|
189
|
+
dark: "#4A4A52",
|
|
190
|
+
} as const;
|
|
191
|
+
|
|
192
|
+
/** Fill for an unlit track segment, for the resolved mode. */
|
|
193
|
+
export function trackFill(): string {
|
|
194
|
+
return TRACK_FILL[detected];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Effort's colour ramp — TEXT only, never a fill, so a row never paints two blocks.
|
|
199
|
+
*
|
|
200
|
+
* Effort is an ORDINAL, so these are a ramp rather than four unrelated hues. The ramp avoids
|
|
201
|
+
* green and red, which already mean "on" and "failed" elsewhere on the same screen: one
|
|
202
|
+
* colour, one meaning, app-wide. Dark runs brighter than light for the reason `MODEL_BADGE`
|
|
203
|
+
* gives — a dim colour on a dark page is a grey one.
|
|
204
|
+
*/
|
|
205
|
+
export const EFFORT_INK = {
|
|
206
|
+
low: { light: "#6B7280", dark: "#9AA0AC" },
|
|
207
|
+
medium: { light: "#0E7490", dark: "#6FD9F0" },
|
|
208
|
+
high: { light: "#B45309", dark: "#F2B25E" },
|
|
209
|
+
xhigh: { light: "#A21CAF", dark: "#E49BFF" },
|
|
210
|
+
} as const;
|
|
211
|
+
|
|
212
|
+
export type EffortInkKind = keyof typeof EFFORT_INK;
|
|
213
|
+
|
|
214
|
+
/** Ink for one effort level, for the resolved mode. Unknown words take the lowest step. */
|
|
215
|
+
export function effortInk(effort: string): string {
|
|
216
|
+
const key = (effort in EFFORT_INK ? effort : "low") as EffortInkKind;
|
|
217
|
+
return EFFORT_INK[key][detected];
|
|
218
|
+
}
|
|
219
|
+
|
|
104
220
|
/**
|
|
105
221
|
* The two reference pages, mirrored from `theme.ts`'s `CONTRAST_REFERENCE`.
|
|
106
222
|
*
|
package/src/ui/theme.ts
CHANGED
|
@@ -59,6 +59,22 @@ export const brand = {
|
|
|
59
59
|
muted: "#6B7280", // 4.53 / 3.52 — de-emphasis, still legible on both
|
|
60
60
|
/** Ink on OUR badges. We own both sides, so contrast is deterministic. */
|
|
61
61
|
ink: "#FFFFFF",
|
|
62
|
+
/**
|
|
63
|
+
* De-emphasised ink on the SELECTION fill, and nowhere else.
|
|
64
|
+
*
|
|
65
|
+
* Measured on a screenshot: `muted` (#6B7280) on the selection purple is
|
|
66
|
+
* 1.11:1, `warning` 1.07:1, `danger` 1.11:1 — every meta tone was invisible
|
|
67
|
+
* the moment its row was selected, and `(default)` on the Models screen
|
|
68
|
+
* disappeared entirely. The page-level greys cannot help here: they are
|
|
69
|
+
* chosen to sit between a cream and a near-black terminal, and the selection
|
|
70
|
+
* is neither.
|
|
71
|
+
*
|
|
72
|
+
* 3.64:1 on the selection fill, and 1.48:1 against `ink` — far enough from
|
|
73
|
+
* white to still read as secondary. Like `ink`, it is measured against a fill
|
|
74
|
+
* WE paint rather than against the terminal, so it is exempt from the
|
|
75
|
+
* both-backgrounds rule the accents above obey.
|
|
76
|
+
*/
|
|
77
|
+
selectionDim: "#D4D4D8",
|
|
62
78
|
} as const;
|
|
63
79
|
|
|
64
80
|
/** Any colour a component may hand to a JSX colour prop. */
|
|
@@ -88,9 +104,19 @@ export const theme = {
|
|
|
88
104
|
info: brand.info,
|
|
89
105
|
},
|
|
90
106
|
|
|
107
|
+
/**
|
|
108
|
+
* The selected row owns the colour channel.
|
|
109
|
+
*
|
|
110
|
+
* A row's normal palette is chosen against the TERMINAL's background; on the
|
|
111
|
+
* selection fill none of it clears even 1.2:1. So a selected row has exactly
|
|
112
|
+
* two inks — `fg` for what it is saying, `dim` for what it is saying quietly —
|
|
113
|
+
* and any tone a row would otherwise carry is surrendered while it is
|
|
114
|
+
* selected. The words survive; the hue does not, and cannot.
|
|
115
|
+
*/
|
|
91
116
|
selection: {
|
|
92
117
|
bg: brand.accent,
|
|
93
118
|
fg: brand.ink,
|
|
119
|
+
dim: brand.selectionDim,
|
|
94
120
|
},
|
|
95
121
|
|
|
96
122
|
/** Blue / green / amber: the widest hue separation available, because these
|