claudeup 6.3.0 → 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 +38 -2
- 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/__tests__/version-snapshot.test.ts +2 -4
- 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 +16 -7
- 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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in presets and the shared agent → tier table.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written constants rather than a glob over data files, for the same reason
|
|
5
|
+
* `src/data/styles/index.ts` hand-lists its embedded presets: a glob cannot resolve at
|
|
6
|
+
* `bun build --compile` time, and this module has to survive into the compiled binary — the
|
|
7
|
+
* hook path reads a project's own `models.json`, but `claudeup models list` and the TUI read
|
|
8
|
+
* these.
|
|
9
|
+
*
|
|
10
|
+
* Tiers name what a subagent is FOR, on one axis: `smart`, `normal`, `cheap`, plus `main` for
|
|
11
|
+
* the orchestrator. Which model serves each is the PRESET's call, and `smart` is not always
|
|
12
|
+
* its largest spend: `fable-advisor` sends the judgement calls to a different model (fable at
|
|
13
|
+
* `medium`) rather than a harder-working one. What holds everywhere is the other half: the
|
|
14
|
+
* thread dispatching the work is never strictly the biggest spend in the preset.
|
|
15
|
+
*/
|
|
16
|
+
import type { Grade, ModelsConfig } from "../services/models-core.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Which tier each subagent gets, shared by every preset.
|
|
20
|
+
*
|
|
21
|
+
* Keys are `subagent_type` strings exactly as the Agent tool receives them: bare names for
|
|
22
|
+
* Claude Code's built-ins, `plugin:name` for plugin agents. A name absent from this table
|
|
23
|
+
* falls to the preset's `fallback`, so a newly installed plugin routes sensibly on day one
|
|
24
|
+
* instead of silently inheriting the orchestrator's model.
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_AGENT_GRADES: Record<string, Grade> = {
|
|
27
|
+
// `smart` is for JUDGEMENT — work where the agent decides something a person would
|
|
28
|
+
// argue about, and where being wrong is expensive to undo.
|
|
29
|
+
Plan: "smart",
|
|
30
|
+
"dev:architect": "smart",
|
|
31
|
+
"dev:reviewer": "smart",
|
|
32
|
+
"dev:debugger": "smart",
|
|
33
|
+
"multimodel:deep-analyst": "smart",
|
|
34
|
+
|
|
35
|
+
// `normal`: transforms material it is GIVEN. Real work, bounded judgement.
|
|
36
|
+
//
|
|
37
|
+
// `spec-writer` reads an interview log and writes it up as a spec — the decisions were
|
|
38
|
+
// made during the interview, and this turns answers into structure. `synthesizer` merges
|
|
39
|
+
// reviews it is handed into one report against thresholds it is handed. Neither decides
|
|
40
|
+
// what is true; both were on `smart` because "writes an important document" got confused
|
|
41
|
+
// with "makes an expensive judgement".
|
|
42
|
+
"dev:spec-writer": "normal",
|
|
43
|
+
"dev:synthesizer": "normal",
|
|
44
|
+
|
|
45
|
+
// Reading a lot is not analysing.
|
|
46
|
+
//
|
|
47
|
+
// `detective` is read-only: it locates implementations, traces a path and reports
|
|
48
|
+
// file:line. That is high-volume retrieval with light inference — the same shape as
|
|
49
|
+
// `Explore`, with more structure in the output. It sat on `smart` because "investigates"
|
|
50
|
+
// sounds like thinking; what it actually does is look.
|
|
51
|
+
"code-analysis:detective": "cheap",
|
|
52
|
+
|
|
53
|
+
// Search, transcription and bookkeeping: bounded, mechanical, high-volume.
|
|
54
|
+
Explore: "cheap",
|
|
55
|
+
"dev:scribe": "cheap",
|
|
56
|
+
"dev:stack-detector": "cheap",
|
|
57
|
+
"dev:docs": "cheap",
|
|
58
|
+
"terminal:tui-navigator": "cheap",
|
|
59
|
+
"gtd:gtd-reviewer": "cheap",
|
|
60
|
+
"statusline-setup": "cheap",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The three workflows the `dev` plugin actually runs, and the agents each one dispatches.
|
|
65
|
+
*
|
|
66
|
+
* A single "17 agents" bar counts every agent claudeup has ever heard of, weighted equally —
|
|
67
|
+
* which is not how a session spends anything. Nobody runs all seventeen; they run ONE of
|
|
68
|
+
* these, and what it costs depends on which handful it reaches for. `/dev:investigate`
|
|
69
|
+
* touching two cheap agents and `/dev:dev` touching five across three tiers are different
|
|
70
|
+
* answers to "what will this preset cost me", and the lump sum shows neither.
|
|
71
|
+
*
|
|
72
|
+
* Read out of `plugins/dev/commands/{dev,debug,investigate}.md` — agents only. Those files
|
|
73
|
+
* also name skills (`dev:context-detection`, `code-analysis:investigate`) which are not
|
|
74
|
+
* dispatched through the Agent tool and so are not routed at all.
|
|
75
|
+
*/
|
|
76
|
+
export const WORKFLOWS: { name: string; agents: string[] }[] = [
|
|
77
|
+
{
|
|
78
|
+
name: "dev",
|
|
79
|
+
agents: [
|
|
80
|
+
"dev:stack-detector",
|
|
81
|
+
"dev:architect",
|
|
82
|
+
"dev:developer",
|
|
83
|
+
"dev:test-architect",
|
|
84
|
+
"dev:reviewer",
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "debug",
|
|
89
|
+
agents: [
|
|
90
|
+
"dev:stack-detector",
|
|
91
|
+
"code-analysis:detective",
|
|
92
|
+
"dev:debugger",
|
|
93
|
+
"dev:developer",
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "investigate",
|
|
98
|
+
agents: ["code-analysis:detective", "dev:researcher"],
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Agents whose model is a CAPABILITY choice, not a spend choice.
|
|
104
|
+
*
|
|
105
|
+
* The three tiers answer "how much model", and that is the wrong question for an agent that
|
|
106
|
+
* reads images. `designer:design-review` compares a rendered screen against a reference and
|
|
107
|
+
* `designer:ui` audits a screenshot for usability: what they need is the model that SEES
|
|
108
|
+
* best, which is not a point on a cheap-to-smart line and does not move when the preset
|
|
109
|
+
* changes.
|
|
110
|
+
*
|
|
111
|
+
* They are deliberately absent from the tier table above and from this map until the model
|
|
112
|
+
* is chosen from the live catalogue rather than from memory. Absent means they fall to the
|
|
113
|
+
* preset's `fallback`, which is a defensible default and an honest one; pinning them to a
|
|
114
|
+
* model id guessed here is neither.
|
|
115
|
+
*/
|
|
116
|
+
export const VISION_AGENTS = ["designer:design-review", "designer:ui"] as const;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* `seo:editor` is the one agent in this marketplace whose own frontmatter sets a model.
|
|
120
|
+
* Routing it to `inherit` keeps that authoring decision visible and working instead of
|
|
121
|
+
* silently overriding it — per-invocation beats frontmatter, so without this the author's
|
|
122
|
+
* choice would vanish with no error.
|
|
123
|
+
*/
|
|
124
|
+
const FRONTMATTER_RESPECTED: Record<string, { model: "inherit" }> = {
|
|
125
|
+
"seo:editor": { model: "inherit" },
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
function preset(
|
|
129
|
+
name: string,
|
|
130
|
+
main: ModelsConfig["main"],
|
|
131
|
+
grades: ModelsConfig["grades"],
|
|
132
|
+
): ModelsConfig {
|
|
133
|
+
return {
|
|
134
|
+
version: 1,
|
|
135
|
+
preset: name,
|
|
136
|
+
main,
|
|
137
|
+
grades,
|
|
138
|
+
agents: { ...DEFAULT_AGENT_GRADES, ...FRONTMATTER_RESPECTED },
|
|
139
|
+
fallback: "normal",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* What each preset is CALLED on screen.
|
|
145
|
+
*
|
|
146
|
+
* The id (`fable-advisor`) is the machine name: it is what `models use` takes, what lands in
|
|
147
|
+
* the committed config, and what must never change. The label is what a person reads, and it
|
|
148
|
+
* says which model leads and which one helps — the thing you actually choose between. Kept
|
|
149
|
+
* beside the presets rather than in the UI so the CLI and the TUI cannot disagree.
|
|
150
|
+
*/
|
|
151
|
+
export const PRESET_LABELS: Record<string, string> = {
|
|
152
|
+
"fable-advisor": "Opus with Fable help",
|
|
153
|
+
"fable-lead": "Fable main",
|
|
154
|
+
"fable-top": "Fable top",
|
|
155
|
+
"opus-lead": "Opus main",
|
|
156
|
+
"sonnet-economy": "Sonnet",
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** The screen name for a preset id, falling back to the id for a hand-edited config. */
|
|
160
|
+
export function presetLabel(id: string): string {
|
|
161
|
+
return PRESET_LABELS[id] ?? id;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The shipped presets.
|
|
166
|
+
*
|
|
167
|
+
* Effort is set per seat and reaches subagents through `modelSettings` — measured by
|
|
168
|
+
* `benches/agent-model-routing/` (AMR-1), which found alias keys honoured and a routed
|
|
169
|
+
* subagent running at its model's effort while the main thread kept its own.
|
|
170
|
+
*
|
|
171
|
+
* ## Effort belongs to the MODEL, so a preset gets one value per model
|
|
172
|
+
*
|
|
173
|
+
* `modelSettings` is keyed by model. Two seats naming the same model therefore cannot carry
|
|
174
|
+
* different efforts, and the validator rejects that rather than letting one silently win.
|
|
175
|
+
* That is the only cross-seat rule here: **within one preset, a model appears at exactly one
|
|
176
|
+
* effort.** Which effort is a per-preset choice — `fable-lead` runs fable at `medium` and
|
|
177
|
+
* `fable-top` runs it at `xhigh`, and that difference is the whole distinction between them.
|
|
178
|
+
*
|
|
179
|
+
* Presets never collide with each other, because only one is ever applied to a project.
|
|
180
|
+
*
|
|
181
|
+
* ## The levelling these presets apply
|
|
182
|
+
*
|
|
183
|
+
* Each one fixes every model it names at a single effort, which is what makes it writable:
|
|
184
|
+
*
|
|
185
|
+
* opus xhigh wherever it appears
|
|
186
|
+
* fable medium in `fable-lead`, xhigh in `fable-top`
|
|
187
|
+
* sonnet high in the `cheap` seat, xhigh when it holds the whole preset
|
|
188
|
+
* haiku not used
|
|
189
|
+
*
|
|
190
|
+
* A tier therefore differs from its neighbour by MODEL, not by a throttle on one model. That
|
|
191
|
+
* is not a style choice: a ramp like "fable xhigh on `smart`, fable low on `cheap`" is exactly
|
|
192
|
+
* what the `modelSettings` keying cannot express.
|
|
193
|
+
*/
|
|
194
|
+
export const BUILT_IN_PRESETS: ModelsConfig[] = [
|
|
195
|
+
// The default: opus runs everything except the judgement calls, which go to fable.
|
|
196
|
+
//
|
|
197
|
+
// Opus takes `main` and `normal` at `xhigh`, fable takes `smart` at its ordinary `medium`,
|
|
198
|
+
// and sonnet picks up the cheap seat at `high`. The id says what fable DOES here — it
|
|
199
|
+
// advises on the hard work rather than running the thread.
|
|
200
|
+
preset(
|
|
201
|
+
"fable-advisor",
|
|
202
|
+
{ model: "opus", effort: "xhigh" },
|
|
203
|
+
{
|
|
204
|
+
smart: { model: "fable", effort: "medium" },
|
|
205
|
+
normal: { model: "opus", effort: "xhigh" },
|
|
206
|
+
cheap: { model: "sonnet", effort: "high" },
|
|
207
|
+
},
|
|
208
|
+
),
|
|
209
|
+
|
|
210
|
+
// Fable runs everything it can, at its ordinary `medium`, with sonnet on the cheap seat.
|
|
211
|
+
//
|
|
212
|
+
// "Fable on the main orchestrator" — the request, literally, extended to the tiers it
|
|
213
|
+
// dispatches. Paired with `fable-top` below: the same model on `main` and `smart`, one
|
|
214
|
+
// effort apart.
|
|
215
|
+
preset(
|
|
216
|
+
"fable-lead",
|
|
217
|
+
{ model: "fable", effort: "medium" },
|
|
218
|
+
{
|
|
219
|
+
smart: { model: "fable", effort: "medium" },
|
|
220
|
+
normal: { model: "fable", effort: "medium" },
|
|
221
|
+
cheap: { model: "sonnet", effort: "high" },
|
|
222
|
+
},
|
|
223
|
+
),
|
|
224
|
+
|
|
225
|
+
// Fable at full stretch on the thread and the judgement calls; opus does the building.
|
|
226
|
+
//
|
|
227
|
+
// `fable-lead` runs fable at `medium` in three seats; this runs it at `xhigh` in two and
|
|
228
|
+
// hands `normal` — the highest-volume tier, since it is also the `fallback` — to opus.
|
|
229
|
+
// The difference between the two presets is fable's effort, which is why it has to BE two
|
|
230
|
+
// presets rather than one with a ramp.
|
|
231
|
+
preset(
|
|
232
|
+
"fable-top",
|
|
233
|
+
{ model: "fable", effort: "xhigh" },
|
|
234
|
+
{
|
|
235
|
+
smart: { model: "fable", effort: "xhigh" },
|
|
236
|
+
normal: { model: "opus", effort: "xhigh" },
|
|
237
|
+
cheap: { model: "sonnet", effort: "high" },
|
|
238
|
+
},
|
|
239
|
+
),
|
|
240
|
+
|
|
241
|
+
// Opus everywhere it is reached at all, at `xhigh`; sonnet only on the cheap seat.
|
|
242
|
+
//
|
|
243
|
+
// `main`, `smart` and `normal` are one spend decision wearing three names. That is legal
|
|
244
|
+
// precisely because they agree on effort, and it is the point of the preset rather than a
|
|
245
|
+
// compromise: the only thing NOT on opus is the searching and transcription.
|
|
246
|
+
preset(
|
|
247
|
+
"opus-lead",
|
|
248
|
+
{ model: "opus", effort: "xhigh" },
|
|
249
|
+
{
|
|
250
|
+
smart: { model: "opus", effort: "xhigh" },
|
|
251
|
+
normal: { model: "opus", effort: "xhigh" },
|
|
252
|
+
cheap: { model: "sonnet", effort: "high" },
|
|
253
|
+
},
|
|
254
|
+
),
|
|
255
|
+
|
|
256
|
+
// Sonnet, at `xhigh`, in all four seats.
|
|
257
|
+
//
|
|
258
|
+
// This preset deliberately has no tier differentiation: its saving is the model, and the
|
|
259
|
+
// effort is not throttled on top of it. Every routed agent runs sonnet at `xhigh`,
|
|
260
|
+
// including `cheap` — so the hook still pins the model (a session started on opus does
|
|
261
|
+
// not leak into its subagents) while the tier table itself makes no distinction.
|
|
262
|
+
preset(
|
|
263
|
+
"sonnet-economy",
|
|
264
|
+
{ model: "sonnet", effort: "xhigh" },
|
|
265
|
+
{
|
|
266
|
+
smart: { model: "sonnet", effort: "xhigh" },
|
|
267
|
+
normal: { model: "sonnet", effort: "xhigh" },
|
|
268
|
+
cheap: { model: "sonnet", effort: "xhigh" },
|
|
269
|
+
},
|
|
270
|
+
),
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
export const DEFAULT_PRESET = "fable-advisor";
|
|
274
|
+
|
|
275
|
+
export function findPreset(name: string): ModelsConfig | undefined {
|
|
276
|
+
return BUILT_IN_PRESETS.find((p) => p.preset === name);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function presetNames(): string[] {
|
|
280
|
+
return BUILT_IN_PRESETS.map((p) => p.preset);
|
|
281
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ModelsConfig } from "../services/models-core.js";
|
|
2
|
+
|
|
1
3
|
export interface PredefinedProfile {
|
|
2
4
|
id: string;
|
|
3
5
|
name: string;
|
|
@@ -7,6 +9,13 @@ export interface PredefinedProfile {
|
|
|
7
9
|
anthropicPlugins: string[];
|
|
8
10
|
skills: string[];
|
|
9
11
|
settings: Record<string, unknown>;
|
|
12
|
+
/**
|
|
13
|
+
* Per-subagent model routing a profile inherits via `extends`. None of the
|
|
14
|
+
* profiles below declare one — routing is chosen per project by `claudeup
|
|
15
|
+
* models use`, not bundled with a plugin set — but the field is here so the
|
|
16
|
+
* inheritance path in `resolveExtends` is real rather than notional.
|
|
17
|
+
*/
|
|
18
|
+
models?: ModelsConfig;
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
@@ -15,7 +24,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
15
24
|
name: "Must Have",
|
|
16
25
|
description: "Essential plugins every Claude Code user should have",
|
|
17
26
|
icon: "★",
|
|
18
|
-
magusPlugins: ["
|
|
27
|
+
magusPlugins: ["setup", "multimodel"],
|
|
19
28
|
anthropicPlugins: [
|
|
20
29
|
"claude-code-setup",
|
|
21
30
|
"claude-md-management",
|
|
@@ -41,7 +50,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
41
50
|
"Must Have + full dev toolkit: code analysis, browser, terminal, design, review",
|
|
42
51
|
icon: "▶",
|
|
43
52
|
magusPlugins: [
|
|
44
|
-
"
|
|
53
|
+
"setup",
|
|
45
54
|
"multimodel",
|
|
46
55
|
"code-analysis",
|
|
47
56
|
"dev",
|
|
@@ -84,7 +93,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
84
93
|
"dev",
|
|
85
94
|
"code-analysis",
|
|
86
95
|
"terminal",
|
|
87
|
-
"
|
|
96
|
+
"setup",
|
|
88
97
|
"designer",
|
|
89
98
|
"browser-use",
|
|
90
99
|
"multimodel",
|
|
@@ -125,7 +134,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
125
134
|
"dev",
|
|
126
135
|
"code-analysis",
|
|
127
136
|
"terminal",
|
|
128
|
-
"
|
|
137
|
+
"setup",
|
|
129
138
|
"multimodel",
|
|
130
139
|
"gtd",
|
|
131
140
|
],
|
|
@@ -160,7 +169,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
160
169
|
"dev",
|
|
161
170
|
"code-analysis",
|
|
162
171
|
"terminal",
|
|
163
|
-
"
|
|
172
|
+
"setup",
|
|
164
173
|
"multimodel",
|
|
165
174
|
"gtd",
|
|
166
175
|
"browser-use",
|
|
@@ -197,7 +206,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
197
206
|
"dev",
|
|
198
207
|
"code-analysis",
|
|
199
208
|
"terminal",
|
|
200
|
-
"
|
|
209
|
+
"setup",
|
|
201
210
|
"seo@magus-marketing",
|
|
202
211
|
"browser-use",
|
|
203
212
|
"image-generate@magus-marketing",
|
|
@@ -232,7 +241,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
232
241
|
"dev",
|
|
233
242
|
"code-analysis",
|
|
234
243
|
"terminal",
|
|
235
|
-
"
|
|
244
|
+
"setup",
|
|
236
245
|
"multimodel",
|
|
237
246
|
"gtd",
|
|
238
247
|
],
|
|
@@ -66,6 +66,7 @@ export const SETTINGS_CATALOG: SettingDefinition[] = [
|
|
|
66
66
|
{ label: "Low", value: "low" },
|
|
67
67
|
{ label: "Medium", value: "medium" },
|
|
68
68
|
{ label: "High", value: "high" },
|
|
69
|
+
{ label: "Extra high", value: "xhigh" },
|
|
69
70
|
],
|
|
70
71
|
storage: { type: "setting", key: "effortLevel" },
|
|
71
72
|
},
|
|
@@ -135,14 +136,20 @@ export const SETTINGS_CATALOG: SettingDefinition[] = [
|
|
|
135
136
|
{
|
|
136
137
|
id: "model",
|
|
137
138
|
name: "Default Model",
|
|
138
|
-
|
|
139
|
+
// Aliases, never pinned ids. A pinned id in a shipped list goes stale the
|
|
140
|
+
// day Anthropic moves the alias, and goes stale SILENTLY — the setting
|
|
141
|
+
// keeps naming a model that is a release behind, and nothing reports it.
|
|
142
|
+
// The alias always names whatever that tier currently is.
|
|
143
|
+
description:
|
|
144
|
+
"Override the default model for Claude Code. Aliases track the current model for each tier",
|
|
139
145
|
category: "models",
|
|
140
146
|
type: "select",
|
|
141
147
|
options: [
|
|
142
148
|
{ label: "Default", value: "" },
|
|
143
|
-
{ label: "
|
|
144
|
-
{ label: "
|
|
145
|
-
{ label: "Haiku", value: "
|
|
149
|
+
{ label: "Opus", value: "opus" },
|
|
150
|
+
{ label: "Sonnet", value: "sonnet" },
|
|
151
|
+
{ label: "Haiku", value: "haiku" },
|
|
152
|
+
{ label: "Fable", value: "fable" },
|
|
146
153
|
],
|
|
147
154
|
storage: { type: "setting", key: "model" },
|
|
148
155
|
},
|
package/src/main.tsx
CHANGED
|
@@ -1,29 +1,68 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { RGBA, createCliRenderer } from "@opentui/core";
|
|
4
|
-
import { createRoot } from "@opentui/react";
|
|
5
3
|
// Static import so `bun build --compile` embeds package.json into the binary;
|
|
6
4
|
// a dynamic require("../package.json") is not resolvable inside the bunfs root.
|
|
7
5
|
import pkg from "../package.json";
|
|
8
|
-
import { route } from "./cli/router.js";
|
|
9
6
|
import { loadProjectDotenv } from "./services/dotenv.js";
|
|
10
|
-
import { App } from "./ui/App.js";
|
|
11
|
-
import { setThemeMode } from "./ui/theme-mode.js";
|
|
12
|
-
import { resolveTheme, snapshotThemeEnv } from "./ui/theme-resolve.js";
|
|
13
7
|
|
|
14
8
|
export const VERSION = (pkg as { version: string }).version;
|
|
15
9
|
|
|
16
10
|
// Note: OpenTUI renderer handles alternate screen buffer automatically
|
|
17
11
|
// No need for manual ANSI escape codes
|
|
12
|
+
//
|
|
13
|
+
// NOTHING in this file may statically import @opentui/* or ./ui/*. `claudeup
|
|
14
|
+
// hook …` runs on every matching Claude Code tool call, and a static import
|
|
15
|
+
// would load the whole TUI module graph before the hook could answer. The TUI
|
|
16
|
+
// (./tui.js), the router (./cli/router.js, which reaches @opentui through
|
|
17
|
+
// cli/update.ts → ui/theme.ts) and the hook itself are all imported on demand,
|
|
18
|
+
// after the branch that decides which one is needed.
|
|
19
|
+
// Pinned by __tests__/hook-import-policy.test.ts.
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `hook` as the first non-theme token → the argv the hook command should see.
|
|
23
|
+
* Anything else → null.
|
|
24
|
+
*
|
|
25
|
+
* A leading `--theme light|dark` (or `--theme=…`) pair is skipped so this
|
|
26
|
+
* matches the router's own dispatch, which drops the theme tokens before
|
|
27
|
+
* looking at the command. Deliberately NOT `parseThemeFlag` from the router:
|
|
28
|
+
* importing that module is the cost this branch exists to avoid, and a hook
|
|
29
|
+
* has no use for a validated theme.
|
|
30
|
+
*/
|
|
31
|
+
function hookArgv(argv: string[]): string[] | null {
|
|
32
|
+
let i = 0;
|
|
33
|
+
while (i < argv.length) {
|
|
34
|
+
const token = argv[i] as string;
|
|
35
|
+
if (token === "--theme") {
|
|
36
|
+
i += 2;
|
|
37
|
+
} else if (token.startsWith("--theme=")) {
|
|
38
|
+
i += 1;
|
|
39
|
+
} else {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return argv[i] === "hook" ? argv.slice(i + 1) : null;
|
|
44
|
+
}
|
|
18
45
|
|
|
19
46
|
async function main(): Promise<void> {
|
|
20
47
|
// FIRST, before anything can touch process.env: the theme is read from the
|
|
21
48
|
// environment claudeup was started with, never from a project's .env. The
|
|
22
49
|
// dotenv load below merges into process.env, so the only thing separating
|
|
23
50
|
// the two is that this line runs before it (pinned by theme-env.test.ts).
|
|
51
|
+
const { snapshotThemeEnv } = await import("./ui/theme-resolve.js");
|
|
24
52
|
const themeEnv = snapshotThemeEnv();
|
|
25
53
|
const args = process.argv.slice(2);
|
|
26
54
|
|
|
55
|
+
// Hooks answer here and go no further. This runs BEFORE loadProjectDotenv()
|
|
56
|
+
// on purpose: a hook must never open a project's .env, because that file is
|
|
57
|
+
// often a symlink to a 1Password FIFO and reading one BLOCKS until a human
|
|
58
|
+
// approves it — on every tool call. The hook needs no .env, so it never
|
|
59
|
+
// touches one.
|
|
60
|
+
const hookRest = hookArgv(args);
|
|
61
|
+
if (hookRest !== null) {
|
|
62
|
+
const { runHookCommand } = await import("./cli/hook.js");
|
|
63
|
+
process.exit(await runHookCommand(hookRest));
|
|
64
|
+
}
|
|
65
|
+
|
|
27
66
|
// Load the project's .env ourselves. Bun's standalone autoload is compiled OFF
|
|
28
67
|
// (scripts/build-binaries.ts) because it killed claudeup outright wherever
|
|
29
68
|
// .env was a symlink to a FIFO — the shape 1Password uses — exiting 1 with no
|
|
@@ -44,6 +83,7 @@ async function main(): Promise<void> {
|
|
|
44
83
|
// Dispatch non-interactive subcommands (claude, update, install, …) and
|
|
45
84
|
// top-level flags (--version/--help). A bare invocation falls through to
|
|
46
85
|
// the interactive TUI below.
|
|
86
|
+
const { route } = await import("./cli/router.js");
|
|
47
87
|
const outcome = await route(args, VERSION);
|
|
48
88
|
if (outcome.handled) {
|
|
49
89
|
process.exit(outcome.exitCode ?? 0);
|
|
@@ -54,85 +94,14 @@ async function main(): Promise<void> {
|
|
|
54
94
|
const isInteractive =
|
|
55
95
|
process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
56
96
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// element now names an adaptive colour explicitly — see src/ui/theme.ts.
|
|
63
|
-
// The mouse is ON, everywhere — it carries real features now: the wheel
|
|
64
|
-
// scrolls the pane under the cursor, and a drag selects text from ONE pane
|
|
65
|
-
// at a time (terminal-native selection is a screen-wide rectangle that
|
|
66
|
-
// grabs both columns), OSC 52-copied on release below.
|
|
67
|
-
//
|
|
68
|
-
// This deliberately includes tmux. Two earlier revisions got this wrong in
|
|
69
|
-
// opposite directions: one disabled the mouse everywhere on the theory that
|
|
70
|
-
// capture breaks tmux-level pane clicking, the next kept it off only inside
|
|
71
|
-
// tmux. Both dated from when claudeup had no mouse features, so disabling
|
|
72
|
-
// cost nothing. MEASURED on a heavily tmux'd machine: tmux forwards mouse
|
|
73
|
-
// to a pane application that asks for it (htop in the same session took
|
|
74
|
-
// clicks and wheel fine while claudeup sat inert), and tmux's own `mouse`
|
|
75
|
-
// option keeps governing pane management. Terminal-native selection stays
|
|
76
|
-
// reachable via Shift+drag in most terminals.
|
|
77
|
-
//
|
|
78
|
-
// enableMouseMovement (mode-1003 "report all motion") stays off — selection
|
|
79
|
-
// needs only button-held drag events, which button reporting delivers.
|
|
80
|
-
const renderer = await createCliRenderer({
|
|
81
|
-
backgroundColor: RGBA.defaultBackground(),
|
|
82
|
-
useMouse: true,
|
|
83
|
-
enableMouseMovement: false,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Mouse selection → system clipboard, on release. OSC 52 survives SSH;
|
|
87
|
-
// terminals that block it simply ignore the sequence. Never fires where the
|
|
88
|
-
// mouse is off, so the tmux path pays nothing.
|
|
89
|
-
renderer.on("selection", (selection: { getSelectedText(): string }) => {
|
|
90
|
-
const text = selection?.getSelectedText() ?? "";
|
|
91
|
-
if (text.length > 0) renderer.copyToClipboardOSC52(text);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Resolve light vs dark once: --theme, CLAUDEUP_THEME, TERM_THEME, the OSC
|
|
95
|
-
// probe, COLORFGBG, dark — see src/ui/theme-resolve.ts. Only the disabled-row
|
|
96
|
-
// tint and the chips need this (src/ui/theme-mode.ts explains why that one
|
|
97
|
-
// case cannot be solved the way the rest of the palette is).
|
|
98
|
-
//
|
|
99
|
-
// The renderer sends OSC 10/11 at creation regardless: @opentui/core 0.1.107
|
|
100
|
-
// has no option to suppress it. So the rule "TERM_THEME skips the probe" is
|
|
101
|
-
// satisfied by NOT AWAITING — when steps 1-3 answer, `probe` is never called,
|
|
102
|
-
// `waitForThemeMode(250)` never runs, first paint is not delayed, and
|
|
103
|
-
// `renderer.themeMode` is never read anywhere in claudeup. Any late reply the
|
|
104
|
-
// terminal sends is consumed by OpenTUI's own handleSequence and ignored here.
|
|
105
|
-
const { mode } = await resolveTheme({
|
|
106
|
-
flag: outcome.theme,
|
|
97
|
+
// Last possible moment: this is the import that pulls in @opentui and the
|
|
98
|
+
// whole ui/ tree. Everything above runs without them.
|
|
99
|
+
const { startTui } = await import("./tui.js");
|
|
100
|
+
await startTui({
|
|
101
|
+
theme: outcome.theme,
|
|
107
102
|
env: themeEnv,
|
|
108
103
|
isInteractive,
|
|
109
|
-
probe: () => renderer.waitForThemeMode(250).catch(() => null),
|
|
110
104
|
});
|
|
111
|
-
setThemeMode(mode);
|
|
112
|
-
|
|
113
|
-
const root = createRoot(renderer);
|
|
114
|
-
|
|
115
|
-
// Cleanup function to restore terminal
|
|
116
|
-
const cleanup = () => {
|
|
117
|
-
root.unmount();
|
|
118
|
-
renderer.destroy(); // CRITICAL: Never use process.exit() directly
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
// Handle cleanup on exit signals
|
|
122
|
-
const handleExit = () => {
|
|
123
|
-
cleanup();
|
|
124
|
-
// Exit after cleanup completes
|
|
125
|
-
process.exit(0);
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
process.on("SIGINT", handleExit);
|
|
129
|
-
process.on("SIGTERM", handleExit);
|
|
130
|
-
|
|
131
|
-
// Render the OpenTUI app with exit handler
|
|
132
|
-
root.render(<App onExit={handleExit} />);
|
|
133
|
-
|
|
134
|
-
// Wait indefinitely (app runs until user exits)
|
|
135
|
-
await new Promise(() => {});
|
|
136
105
|
}
|
|
137
106
|
|
|
138
107
|
main().catch((error) => {
|