.gitattributes CHANGED
@@ -1,2 +1 @@
1
 
2
- docs/screenshots/alternative-agents.png filter=lfs diff=lfs merge=lfs -text
 
1
 
 
OpenHands-Design/DESIGN.md DELETED
@@ -1,597 +0,0 @@
1
- # OpenHands UI Design System
2
-
3
- ## 1. Visual Theme & Atmosphere
4
-
5
- OpenHands is a dark-first AI agent platform built on a near-black monochrome canvas. The entire experience lives on a `0 0% 5%` HSL background — effectively `#0d0d0d` — with `0 0% 98%` foreground text that reads as warm off-white. Every surface is a shade of neutral grey scaled in 2–5% lightness increments, creating depth through tonal variation rather than color. The only chromatic moments are semantic: green for success, red-orange for danger, amber for warnings, and blue for informational states.
6
-
7
- Typography is carried by **Inter** (sans-serif) for all UI text and **JetBrains Mono** for code, terminals, and technical labels. The type system is weight-restrained — `font-medium` (500) is the workhorse, `font-semibold` (600) for headings and emphasis, and `font-normal` (400) for body. Bold (700) is rare and reserved for maximum emphasis.
8
-
9
- The UI framework is **React + Tailwind CSS + Radix primitives** (shadcn/ui pattern). All colors flow through CSS custom properties declared in `:root` and consumed via `hsl(var(--token))` in the Tailwind config. This means every color in the system is overridable by changing a single HSL triplet.
10
-
11
- **Key characteristics:**
12
- - Near-black monochrome canvas (`#0d0d0d` background, `#fafafa` foreground)
13
- - Neutral grey surface scale in 2–5% lightness increments (5% → 7% → 8% → 12% → 14% → 18%)
14
- - Inter + JetBrains Mono dual-font system
15
- - HSL-based CSS custom property architecture for full theme overridability
16
- - Tailwind utility-first styling with Radix UI headless primitives
17
- - `transition-colors` as the dominant transition (958 uses) — UI feels responsive but not animated
18
- - Dark-only primary mode; light and sepia modes exist as secondary via class-map theming
19
-
20
- ---
21
-
22
- ## 2. Color Palette & Roles
23
-
24
- All colors are declared as HSL triplets (without the `hsl()` wrapper) in CSS custom properties. Tailwind maps them as `hsl(var(--token))`.
25
-
26
- ### Core Surfaces
27
-
28
- | Token | HSL | Hex | Role |
29
- |-------|-----|-----|------|
30
- | `--background` | `0 0% 5%` | `#0d0d0d` | Page background, app shell |
31
- | `--card` | `0 0% 7%` | `#121212` | Card surfaces, elevated containers |
32
- | `--secondary` | `0 0% 8%` | `#141414` | Secondary surfaces, sidebar accent |
33
- | `--popover` | `0 0% 7%` | `#121212` | Dropdown menus, popovers |
34
- | `--muted` | `0 0% 12%` | `#1f1f1f` | Muted backgrounds, hover fills, badges, **tooltip surfaces** |
35
- | `--border` / `--input` | `0 0% 14%` | `#242424` | Borders, input borders, dividers |
36
- | `--muted-hover` | `0 0% 18%` | `#2e2e2e` | Hover state for muted surfaces |
37
- | `--modal-background` | Inherits `--background` | `#0d0d0d` | Dialogs, sheets, modals (can diverge) |
38
-
39
- ### Core Text
40
-
41
- | Token | HSL | Hex | Role |
42
- |-------|-----|-----|------|
43
- | `--foreground` | `0 0% 98%` | `#fafafa` | Primary text, headings |
44
- | `--muted-foreground` | `0 0% 55%` | `#8c8c8c` | Secondary text, labels, placeholders, icons |
45
- | `--primary` | `0 0% 100%` | `#ffffff` | Maximum emphasis text, primary buttons |
46
- | `--primary-foreground` | `0 0% 0%` | `#000000` | Text on primary (white) surfaces |
47
- | `--accent` | `0 0% 100%` | `#ffffff` | Accent elements (matches primary in dark) |
48
-
49
- ### Sidebar (inherits core but isolated for overridability)
50
-
51
- | Token | HSL | Role |
52
- |-------|-----|------|
53
- | `--sidebar-background` | `0 0% 5%` | Sidebar background |
54
- | `--sidebar-foreground` | `0 0% 98%` | Sidebar text |
55
- | `--sidebar-accent` | `0 0% 8%` | Sidebar hover/active background |
56
- | `--sidebar-border` | `0 0% 14%` | Sidebar dividers |
57
- | `--sidebar-ring` | `0 0% 50%` | Sidebar focus ring |
58
-
59
- ### Semantic / Status
60
-
61
- | Token | HSL | Hex | Role |
62
- |-------|-----|-----|------|
63
- | `--success` | `142 71% 45%` | `#22c55e` | Success states, running indicators |
64
- | `--success-foreground` | `142 71% 76%` | `#86efac` | Success text on dark surfaces |
65
- | `--warning` | `38 92% 50%` | `#f59e0b` | Warning states, caution badges |
66
- | `--info` | `217 91% 60%` | `#3b82f6` | Informational states, links |
67
- | `--destructive` | `0 72% 51%` | `#dc2626` | Error states, danger actions, delete |
68
- | `--destructive-foreground` | `0 0% 98%` | `#fafafa` | Text on destructive surfaces |
69
- | `--ring` | `0 0% 80%` | `#cccccc` | Focus rings (1px, keyboard-only via `focus-visible:`) |
70
-
71
- ### Gradients & Decorative
72
-
73
- | Token | Value | Role |
74
- |-------|-------|------|
75
- | `--gradient-card-hover` | `linear-gradient(180deg, hsl(0 0% 9%) 0%, hsl(0 0% 7%) 100%)` | Subtle card hover gradient |
76
- | `--shadow-card` | `0 1px 2px 0 hsl(0 0% 0% / 0.3)` | Default card shadow |
77
-
78
- ### Hover Backgrounds
79
-
80
- | Surface | Hover Token | Use |
81
- |---------|-------------|-----|
82
- | Dark surfaces (cards, nav items, menus, rows) | `hover:bg-muted/60` | **Standard hover** — the single canonical dark-surface hover |
83
- | White/primary buttons | `hover:bg-primary/85` | Light grey hover on white buttons (85% opacity white) |
84
-
85
- **Canonical dark-surface hover: `hover:bg-muted/60`** — used consistently across the codebase. Do **not** mix `/40`, `/50`, `/70` variants.
86
- **Canonical primary-button hover: `hover:bg-primary/85`** — never use `hover:bg-muted/60` on a `bg-primary`/`bg-white` button (causes dark flash).
87
-
88
- ---
89
-
90
- ## 3. Typography Rules
91
-
92
- ### Font Families
93
-
94
- | Role | Family | CSS Variable | Tailwind Class | Fallbacks |
95
- |------|--------|-------------|----------------|-----------|
96
- | UI / Body | Inter | `--font-sans` | `font-sans` | `system-ui, sans-serif` |
97
- | Code / Technical | JetBrains Mono | `--font-mono` | `font-mono` | `monospace` |
98
-
99
- Fonts are loaded via Google Fonts `@import` in `index.css`.
100
-
101
- ### Type Scale
102
-
103
- The app uses Tailwind's default type scale. These are the **canonical sizes** ordered by frequency of use:
104
-
105
- | Tailwind Class | Size | Uses | Role |
106
- |----------------|------|------|------|
107
- | `text-sm` | 14px / 0.875rem | 711 | **Primary body text**, labels, button text, descriptions |
108
- | `text-xs` | 12px / 0.75rem | 427 | **Secondary text**, metadata, badges, menu items, captions |
109
- | `text-base` | 16px / 1rem | 52 | Larger body text, input text, chat messages |
110
- | `text-lg` | 18px / 1.125rem | 67 | Section sub-headings, dialog titles |
111
- | `text-xl` | 20px / 1.25rem | 28 | Page sub-headings |
112
- | `text-2xl` | 24px / 1.5rem | 31 | Page headings, modal titles |
113
- | `text-3xl` | 30px / 1.875rem | 12 | Hero headings, landing sections |
114
-
115
- ### Arbitrary Font Sizes (to normalize)
116
-
117
- These arbitrary sizes appear frequently and should be migrated to the standard scale or formalized as tokens:
118
-
119
- | Arbitrary | Count | Recommended Replacement |
120
- |-----------|-------|------------------------|
121
- | `text-[11px]` | 46 | `text-xs` (12px) — or formalize as `--text-2xs` if 11px is intentional |
122
- | `text-[10px]` | 20 | `text-xs` (12px) — or formalize as `--text-2xs` |
123
- | `text-[12px]` | 8 | `text-xs` (already 12px — use the utility) |
124
- | `text-[40px]` | 5 | `text-4xl` (36px) or formalize as hero display size |
125
- | `text-[28px]` | 3 | `text-3xl` (30px) or formalize |
126
- | `text-[32px]` | 1 | `text-3xl` (30px) or `text-4xl` (36px) |
127
- | `text-[8px]` | 1 | Likely a micro label — evaluate if needed |
128
-
129
- ### Font Weight Scale
130
-
131
- | Tailwind Class | Weight | Uses | Role |
132
- |----------------|--------|------|------|
133
- | `font-medium` | 500 | 304 | Labels, nav items, badges (note: buttons use `font-normal`) |
134
- | `font-semibold` | 600 | 229 | **Headings**, section titles, strong emphasis |
135
- | `font-normal` | 400 | 106 | **Body text**, descriptions, long-form content |
136
- | `font-bold` | 700 | 29 | Maximum emphasis (use sparingly) |
137
- | `font-light` | 300 | 13 | De-emphasized text (use sparingly) |
138
-
139
- ### Line Height
140
-
141
- | Tailwind Class | Uses | Role |
142
- |----------------|------|------|
143
- | `leading-4` | 38 | Tight — compact UI, badges |
144
- | `leading-6` | 34 | Standard — body text |
145
- | `leading-relaxed` | 33 | Comfortable — long-form, descriptions |
146
- | `leading-5` | 28 | Medium — labels, short text |
147
- | `leading-tight` | 27 | Condensed — headings |
148
- | `leading-snug` | 17 | Slightly condensed |
149
- | `leading-none` | 16 | No leading — single-line elements |
150
-
151
- ### Letter Spacing
152
-
153
- | Tailwind Class | Uses | Role |
154
- |----------------|------|------|
155
- | `tracking-wide` | 28 | Uppercase labels, section headers |
156
- | `tracking-wider` | 20 | Small-caps metadata |
157
- | `tracking-tight` | 19 | Display headings |
158
-
159
- ### Canonical Patterns
160
-
161
- **Body text:** `text-sm font-normal text-foreground`
162
- **Label:** `text-sm font-medium text-foreground`
163
- **Secondary text:** `text-sm text-muted-foreground`
164
- **Metadata/caption:** `text-xs text-muted-foreground`
165
- **Uppercase category:** `text-[11px] font-medium uppercase tracking-wide text-muted-foreground`
166
- **Heading (page):** `text-2xl font-semibold text-foreground`
167
- **Heading (section):** `text-lg font-semibold text-foreground`
168
- **Code/mono:** `text-sm font-mono`
169
-
170
- ---
171
-
172
- ## 4. Component Stylings
173
-
174
- ### Buttons (`Button` component — `src/components/ui/button.tsx`)
175
-
176
- **Base classes (all variants):**
177
- `inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-normal ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.97] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`
178
-
179
- | Variant | Background | Text | Border | Hover | Use |
180
- |---------|-----------|------|--------|-------|-----|
181
- | `default` | `bg-primary` | `text-primary-foreground` | — | `hover:bg-primary/85` | Primary CTA (white button, black text) |
182
- | `destructive` | `bg-destructive` | `text-destructive-foreground` | — | `hover:bg-destructive/85` | Delete, danger actions |
183
- | `outline` | `bg-background` | — | `border border-input` | `hover:bg-muted hover:text-foreground` | Secondary actions (most used — 53 instances) |
184
- | `light` | `bg-primary` | `text-primary-foreground` | `border border-input` | `hover:bg-primary/85` | High-contrast primary on dark bg (token-based, no raw `bg-white`) |
185
- | `secondary` | `bg-secondary` | `text-secondary-foreground` | — | `hover:bg-muted-hover` | Tertiary actions |
186
- | `muted` | `bg-muted` | `text-muted-foreground` | — | `hover:bg-muted-hover hover:text-foreground` | Subdued actions |
187
- | `ghost` | transparent | — | — | `hover:bg-muted hover:text-foreground` | Minimal chrome actions |
188
- | `link` | transparent | `text-primary underline-offset-4` | — | `hover:underline` | Inline links |
189
-
190
- **Primary button convention:** All white/primary buttons use `bg-primary text-primary-foreground hover:bg-primary/85`. Never use `bg-white text-black hover:bg-muted/60` inline — the dark hover on a white button is incorrect. Use the `Button` component or match its tokens.
191
-
192
- | Size | Height | Padding | Font |
193
- |------|--------|---------|------|
194
- | `default` | `h-10` | `px-4 py-2` | `text-sm` |
195
- | `sm` | `h-10` | `px-3` | `text-sm` |
196
- | `xs` | `h-10` | `px-3` | `text-xs` |
197
- | `lg` | `h-11` | `px-8` | `text-sm` |
198
- | `icon` | `h-10 w-10` | — | — |
199
-
200
- ### Cards & Containers
201
-
202
- There is no dedicated `Card` primitive — cards are composed with utilities.
203
-
204
- **Standard card recipe:**
205
- ```
206
- bg-card border border-border rounded-lg p-4
207
- ```
208
-
209
- **Elevated card:**
210
- ```
211
- bg-card border border-border rounded-xl p-6 shadow-lg
212
- ```
213
-
214
- **Interactive card:**
215
- ```
216
- bg-card border border-border rounded-lg p-4 transition-colors hover:border-white/30
217
- ```
218
-
219
- **Glass / backdrop card:**
220
- ```
221
- bg-card/70 border border-border/60 rounded-lg p-6 shadow-lg backdrop-blur-xl supports-[backdrop-filter]:bg-card/50
222
- ```
223
-
224
- ### Inputs (`Input` component — `src/components/ui/input.tsx`)
225
-
226
- **Standard input:**
227
- ```
228
- h-10 w-full rounded-md border border-border bg-muted/40 px-3 py-2 text-base md:text-sm
229
- ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
230
- focus-visible:ring-offset-2 focus-visible:bg-muted/60 hover:bg-muted/60
231
- placeholder:text-muted-foreground
232
- disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted/30
233
- ```
234
-
235
- **Canonical focus style (all inputs, textareas, selects must match):**
236
- ```
237
- ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:bg-muted/60
238
- ```
239
-
240
- Key rules:
241
- - Always use `focus-visible:` (keyboard-only), never `focus:` (fires on click too)
242
- - Always include `ring-offset-background` and `focus-visible:ring-offset-2`
243
- - Always include `focus-visible:bg-muted/60` for the subtle fill on focus
244
- - Search inputs (`type="search"`) have `appearance: none` in global CSS to strip browser default focus chrome
245
-
246
- **Size variants (via SearchInput wrapper):**
247
- - `sm`: `h-9` + `pl-8 pr-8` (icon padding)
248
- - `default`: `h-10` + `pl-9 pr-9`
249
- - `lg`: `h-11` + `pl-10 pr-10`
250
-
251
- ### Dropdown Menus (`DropdownMenu` — Radix-based)
252
-
253
- **Menu content:**
254
- ```
255
- z-[100] min-w-[8rem] overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md
256
- ```
257
-
258
- **Menu item:**
259
- ```
260
- group relative flex cursor-default select-none items-center rounded-md px-2 py-1.5 text-sm
261
- transition-colors focus:bg-muted/60 data-[highlighted]:bg-muted/60
262
- ```
263
-
264
- **Icon treatment in menu items:**
265
- - Default: `[&_svg]:text-muted-foreground` (grey)
266
- - Hover/highlight: `group-hover:[&_svg]:!text-foreground` (white)
267
-
268
- ### Popover
269
-
270
- **Content:**
271
- ```
272
- z-50 max-h-[min(24rem,calc(100dvh-2rem))] shadow-md rounded-[12px] border border-border
273
- bg-sidebar p-6 text-sidebar-foreground overflow-y-auto
274
- ```
275
-
276
- ### Navigation (LeftNav sidebar)
277
-
278
- - Collapsed: 56px wide icon rail
279
- - Expanded: 240px+ with text labels
280
- - Items: `flex items-center gap-2 rounded-md px-3 py-1.5 text-xs transition-colors`
281
- - Icons: `w-4 h-4 text-muted-foreground group-hover:text-white`
282
- - Active: `bg-muted/60 text-foreground`
283
- - Hover: `hover:bg-muted/60 hover:text-white`
284
-
285
- ### Scrollbar Variants
286
-
287
- | Class | Width | Behavior | Use |
288
- |-------|-------|----------|-----|
289
- | `.dropdown-scroll` | 6px thin | Always visible | Menus, popovers |
290
- | `.custom-scrollbar` | 8px thin | Always visible | Chat, main content |
291
- | `.scrollbar-on-hover` | 8px thin | Visible on hover only | Chat threads |
292
- | `.hide-scrollbar` | hidden | Hidden | Horizontal scroll areas |
293
-
294
- All scrollbar thumbs: `hsl(var(--muted-foreground) / 0.5)` with hover at `0.7`.
295
-
296
- ### Tooltips
297
-
298
- All tooltips use `bg-muted` for a lighter surface that visually separates from the dark page background.
299
-
300
- **Standard tooltip (rounded-md):**
301
- ```
302
- whitespace-nowrap rounded-md bg-muted px-2 py-1 text-xs text-foreground shadow-md
303
- ```
304
-
305
- **Pill tooltip (rounded-full):**
306
- ```
307
- bg-muted text-foreground text-xs rounded-full shadow-lg px-3 py-1
308
- ```
309
-
310
- ### Dialog Close Button
311
-
312
- The dialog close "×" button has no focus ring (focus ring removed to avoid visual noise on click):
313
- ```
314
- absolute right-4 top-4 inline-flex h-7 w-7 items-center justify-center rounded-md opacity-70
315
- ring-offset-background transition-colors hover:opacity-100 hover:bg-muted/60 focus:outline-none
316
- ```
317
-
318
- ---
319
-
320
- ## 5. Layout Principles
321
-
322
- ### Spacing System
323
-
324
- The app uses Tailwind's default 4px-based spacing scale. These are the most common values by usage:
325
-
326
- **Gaps (flex/grid):**
327
-
328
- | Class | Px | Uses | Context |
329
- |-------|-----|------|---------|
330
- | `gap-2` | 8px | 414 | **Standard gap** — between items in rows, icon + label |
331
- | `gap-3` | 12px | 144 | Comfortable gap — form groups, card content |
332
- | `gap-4` | 16px | 110 | Generous gap — section spacing, grid layouts |
333
- | `gap-1` | 4px | 86 | Tight gap — inline badges, compact lists |
334
- | `gap-1.5` | 6px | 59 | Between tight and standard |
335
- | `gap-6` | 24px | 50 | Large gap — major sections |
336
-
337
- **Padding:**
338
-
339
- | Class | Px | Uses | Context |
340
- |-------|-----|------|---------|
341
- | `px-4` | 16px | 216 | **Standard horizontal padding** — buttons, cards |
342
- | `px-3` | 12px | 212 | Compact horizontal padding — menu items, inputs |
343
- | `py-2` | 8px | 200 | **Standard vertical padding** — buttons, rows |
344
- | `px-2` | 8px | 198 | Tight horizontal padding — badges, pills |
345
- | `py-1` | 4px | 138 | Compact vertical padding |
346
- | `py-1.5` | 6px | 76 | Slightly more than compact |
347
- | `p-4` | 16px | 89 | Uniform card/container padding |
348
- | `p-6` | 24px | 26 | Generous container/dialog padding |
349
-
350
- ### Grid & Container
351
-
352
- - Max container width: `1400px` (via Tailwind `container` config with `2rem` padding)
353
- - Primary layout: sidebar (56–240px) + main content area
354
- - Settings layout: custom CSS vars for independent nav/main vertical inset
355
- - `--settings-nav-padding-top/bottom`: `2rem`
356
- - `--settings-main-padding-top/bottom`: `2rem`
357
-
358
- ### Whitespace Philosophy
359
-
360
- - **Dense but breathable**: The app uses `text-sm` (14px) as the default with `gap-2` (8px) standard spacing — dense enough for a productivity tool, but never cramped.
361
- - **Consistent rhythm**: Sections are separated by `border-t border-border` dividers with `my-3` (12px) vertical margin. No heavy horizontal rules.
362
- - **Surface differentiation over spacing**: Rather than using large whitespace to separate areas, the app uses background color shifts (`bg-background` → `bg-card` → `bg-muted`) to create visual sections.
363
-
364
- ### Border Radius Scale
365
-
366
- Defined via CSS custom properties and Tailwind mapping:
367
-
368
- | Token | Value | Tailwind | Uses | Role |
369
- |-------|-------|----------|------|------|
370
- | `--radius` | `0.375rem` (6px) | `rounded-lg` | 112 | **Standard container radius** — cards, panels |
371
- | `calc(--radius - 2px)` | `0.25rem` (4px) | `rounded-md` | 501 | **Default element radius** — buttons, inputs, menu items |
372
- | `calc(--radius - 4px)` | `0.125rem` (2px) | `rounded-sm` | 12 | Subtle radius — small inline elements |
373
- | `--radius-modal` | `0.75rem` (12px) | `rounded-modal` | — | Modal/dialog/popover radius |
374
- | — | — | `rounded-xl` | 90 | Larger cards, featured containers |
375
- | — | — | `rounded-2xl` | 23 | Hero elements, large cards |
376
- | — | — | `rounded-full` | 185 | Avatars, pills, circular buttons, badges |
377
-
378
- **Arbitrary radii to normalize:**
379
-
380
- | Arbitrary | Count | Recommended |
381
- |-----------|-------|-------------|
382
- | `rounded-[6px]` | 20 | `rounded-lg` (already 6px via `--radius`) |
383
- | `rounded-[100px]` | 15 | `rounded-full` (same visual effect) |
384
- | `rounded-[12px]` | 8 | `rounded-modal` or `rounded-xl` (12px) |
385
- | `rounded-[4px]` | 4 | `rounded-md` (already 4px) |
386
-
387
- ---
388
-
389
- ## 6. Depth & Elevation
390
-
391
- ### Shadow Scale
392
-
393
- | Tailwind | Uses | Role |
394
- |----------|------|------|
395
- | `shadow-sm` | 21 | Subtle elevation — small cards, badges |
396
- | `shadow` | 22 | Default — standalone cards |
397
- | `shadow-md` | 31 | Medium — dropdown menus, popovers |
398
- | `shadow-lg` | 49 | **Most used** — modals, dialogs, elevated panels |
399
- | `shadow-xl` | 14 | High emphasis — floating panels |
400
- | `shadow-2xl` | 5 | Maximum — overlay dialogs |
401
- | `shadow-inner` | 8 | Inset — pressed buttons, input focus |
402
- | `shadow-none` | 19 | Reset — flat elements |
403
-
404
- ### Custom Shadows
405
-
406
- | Token | Value | Role |
407
- |-------|-------|------|
408
- | `--shadow-card` | `0 1px 2px 0 hsl(0 0% 0% / 0.3)` | Card resting shadow |
409
-
410
- ### Elevation Levels
411
-
412
- | Level | Treatment | Use |
413
- |-------|-----------|-----|
414
- | 0 — Flat | No shadow, `bg-background` | Page background |
415
- | 1 — Surface | `bg-card` + `border border-border` | Cards, content panels |
416
- | 2 — Raised | `shadow-md` + `border` | Dropdown menus, popovers |
417
- | 3 — Floating | `shadow-lg` + `border` | Modals, dialogs, sheets |
418
- | 4 — Overlay | `shadow-xl` or `shadow-2xl` | Full-screen overlays, drawers |
419
-
420
- ### Border System
421
-
422
- - **Standard border:** `border border-border` (1px solid `hsl(0 0% 14%)`)
423
- - **Subtle border:** `border border-border/60` (reduced opacity)
424
- - **Interactive hover:** `hover:border-white/30` or `hover:border-muted-foreground/30`
425
- - **Section divider:** `border-t border-border` (horizontal rule) or `border-t border-sidebar-border` (in sidebar)
426
- - **Focus ring:** `ring-1 ring-ring ring-offset-2 ring-offset-background` (1px, `focus-visible:` only)
427
-
428
- ---
429
-
430
- ## 7. Do's and Don'ts
431
-
432
- ### Colors
433
-
434
- | Do | Don't |
435
- |----|-------|
436
- | Use `text-foreground` for primary text | Use `text-white` for primary text (278 instances to migrate) |
437
- | Use `text-muted-foreground` for secondary text | Use `text-stone-400` or `text-gray-400` (raw palette) |
438
- | Use `bg-background` for page surfaces | Use `bg-black` or hardcoded `bg-[#0d0d0d]` |
439
- | Use `bg-card` for elevated surfaces | Use `bg-stone-800` or `bg-neutral-900` |
440
- | Use `bg-muted` for subtle backgrounds | Use `bg-stone-700` or `bg-gray-800` |
441
- | Use `border-border` for all borders | Use `border-stone-700` or `border-gray-700` |
442
- | Use `text-success-foreground` for success text | Use `text-emerald-400` or `text-green-400` |
443
- | Use `text-destructive` for error text | Use `text-red-500` or `text-rose-500` |
444
- | Use `hover:text-foreground` for hover text brightening | Use `hover:text-white` except in sidebar context |
445
-
446
- **Semantic status colors:** Use `text-success` / `bg-success`, `text-warning` / `bg-warning`, `text-info` / `bg-info`, `text-destructive` / `bg-destructive` — never raw chromatic palette classes like `text-green-500`, `bg-amber-400`, `text-blue-500`, etc.
447
-
448
- **Current debt:** `themeAppClassMap.ts` and `NewUserExperienceFlowchart.tsx` still use raw `stone-*` / `rgb()` values (theme definition maps — intentionally deferred; requires per-theme CSS variable architecture). `ChatThread.tsx` `messageTypeColors` has 4 remaining raw palette colors (`orange-500`, `indigo-500`, `purple-500`, `pink-500`) for categorical distinctness — no semantic tokens defined for these yet.
449
-
450
- ### Typography
451
-
452
- | Do | Don't |
453
- |----|-------|
454
- | Use `text-sm` (14px) as default body size | Use `text-[14px]` or arbitrary pixel values |
455
- | Use `text-xs` (12px) for small/meta text | Use arbitrary pixel sizes for general text |
456
- | Use Tailwind scale (`text-lg`, `text-xl`, `text-2xl`) | Use arbitrary sizes like `text-[28px]`, `text-[40px]` |
457
- | Use `font-medium` as default weight | Use `font-bold` for general emphasis |
458
- | Keep heading hierarchy: `2xl` → `xl` → `lg` → `base` | Skip levels or invert the scale |
459
-
460
- ### Border Radius
461
-
462
- | Do | Don't |
463
- |----|-------|
464
- | Use `rounded-md` (4px) for buttons, inputs, menu items | Use `rounded-[4px]` (same value, less maintainable) |
465
- | Use `rounded-lg` (6px) for cards, containers | Use `rounded-[6px]` (use the token) |
466
- | Use `rounded-xl` or `rounded-modal` for dialogs | Use `rounded-[12px]` (use the token) |
467
- | Use `rounded-full` for pills and avatars | Use `rounded-[100px]` (use `rounded-full`) |
468
-
469
- ### Spacing
470
-
471
- | Do | Don't |
472
- |----|-------|
473
- | Use `gap-2` (8px) as standard item gap | Use arbitrary gap values |
474
- | Use `px-3`/`px-4` for horizontal padding | Mix `px-2.5` and `px-3.5` without reason |
475
- | Use `p-4` for card padding, `p-6` for dialogs | Use `p-[24px]` (same as `p-6`) |
476
- | Use `my-3` for section divider spacing | Use inconsistent vertical margins around dividers |
477
-
478
- ### Hover & Interaction
479
-
480
- | Do | Don't |
481
- |----|-------|
482
- | Use `hover:bg-muted/60` as standard hover bg on dark surfaces | Mix `/40`, `/50`, `/60`, `/70` without hierarchy |
483
- | Use `hover:bg-primary/85` for white/primary buttons | Use `hover:bg-muted/60` on white buttons (creates dark hover) |
484
- | Use `transition-colors` for color-only changes | Use `transition-all` when only color changes |
485
- | Use `duration-200` as standard transition speed | Mix `duration-150`, `duration-200`, `duration-300` randomly |
486
- | Use `group` + `group-hover:` for parent-child hover | Apply hover to each child independently |
487
- | Use `active:scale-[0.97]` for button press feedback | Use `active:scale-95` (inconsistent with Button component) |
488
-
489
- ### Icons
490
-
491
- | Do | Don't |
492
- |----|-------|
493
- | Use `w-4 h-4` as standard icon size in menus/buttons | Use `w-3 h-3` or `w-5 h-5` without size hierarchy reason |
494
- | Set icon color to `text-muted-foreground` by default | Leave icons inheriting parent text color (appears too bright) |
495
- | Brighten on hover: `group-hover:text-foreground` or `group-hover:text-white` | Omit icon hover transitions |
496
- | Use `shrink-0` on icons in flex layouts | Let icons squish when text wraps |
497
-
498
- ---
499
-
500
- ## 8. Responsive Behavior
501
-
502
- ### Breakpoints (Tailwind defaults)
503
-
504
- | Prefix | Min Width | Key Changes |
505
- |--------|-----------|-------------|
506
- | (none) | 0px | Mobile-first base styles |
507
- | `sm` | 640px | Wider cards, more padding |
508
- | `md` | 768px | Multi-column layouts begin, `md:text-sm` on inputs |
509
- | `lg` | 1024px | Full sidebar visible, expanded grid |
510
- | `xl` | 1280px | Maximum content width, full feature layout |
511
- | `2xl` | 1400px | Container max-width ceiling |
512
-
513
- ### Touch Targets
514
- - Minimum interactive height: `h-10` (40px) for buttons and inputs
515
- - Small variant: `h-9` (36px) for compact contexts
516
- - Icon buttons: `h-10 w-10` (40×40px)
517
- - Menu items: `py-1.5` (6px) vertical padding at `text-sm` yields ~32px touch target
518
-
519
- ### Collapsing Strategy
520
- - Sidebar: collapses from expanded (labels) to icon-only rail on narrow viewports
521
- - Navigation menus: horizontal → hamburger on mobile
522
- - Grid layouts: multi-column → single-column stacked
523
- - Container padding: reduces from `p-6` → `p-4` → `p-3` at smaller breakpoints
524
-
525
- ---
526
-
527
- ## 9. Interaction & Motion
528
-
529
- ### Transitions
530
-
531
- | Pattern | Uses | When |
532
- |---------|------|------|
533
- | `transition-colors` | 958 | **Default** — use for any color/bg/border change |
534
- | `transition-opacity` | 96 | Fade in/out |
535
- | `transition-all` | 96 | Multiple properties changing simultaneously |
536
- | `transition-transform` | 59 | Scale/translate animations |
537
-
538
- ### Duration
539
-
540
- | Duration | Uses | When |
541
- |----------|------|------|
542
- | `duration-200` | ~48 | **Standard** — local, small feedback: toggles, chevron rotation, sidebar width, card/row hovers, opacity on hover, dialogs |
543
- | `duration-300` | ~34 | **Layout motion** — panel/drawer resize, sheet exit, canvas split, login/marketing card hover, grid row animations |
544
-
545
- ### Easing
546
-
547
- | Easing | Uses | When |
548
- |--------|------|------|
549
- | `ease-in-out` | 111 | **Default** — smooth symmetrical transitions |
550
- | `ease-out` | 52 | Enter animations — elements arriving |
551
-
552
- ### Framer Motion Patterns (23 files)
553
- - `AnimatePresence` for mount/unmount transitions
554
- - Standard enter: `initial={{ opacity: 0 }}` → `animate={{ opacity: 1 }}`
555
- - Standard exit: `exit={{ opacity: 0 }}`
556
- - Duration: typically `0.2s`–`0.3s`
557
- - Used for: panel reveals, notification toasts, drawer slides, loading states
558
-
559
- ### Interactive Feedback
560
- - **Button press:** `active:scale-[0.97]` (slight shrink on click)
561
- - **Card hover:** `hover:scale-[1.02]` (subtle grow, 12 uses)
562
- - **Focus:** `focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2` (1px ring, keyboard-only)
563
-
564
- ---
565
-
566
- ## 10. Agent Prompt Guide
567
-
568
- ### Quick Color Reference
569
- - Page background: `bg-background` → `hsl(0 0% 5%)` → `#0d0d0d`
570
- - Primary text: `text-foreground` → `hsl(0 0% 98%)` → `#fafafa`
571
- - Secondary text: `text-muted-foreground` → `hsl(0 0% 55%)` → `#8c8c8c`
572
- - Card surface: `bg-card` → `hsl(0 0% 7%)` → `#121212`
573
- - Border: `border-border` → `hsl(0 0% 14%)` → `#242424`
574
- - Hover background: `bg-muted/60` → `hsl(0 0% 12% / 0.6)`
575
- - Success: `text-success-foreground` → `hsl(142 71% 76%)` → `#86efac`
576
- - Error: `text-destructive` → `hsl(0 72% 51%)` → `#dc2626`
577
-
578
- ### Example Component Prompts
579
-
580
- - **"Create a settings card"**: `bg-card border border-border rounded-lg p-4`. Title at `text-lg font-semibold text-foreground`. Description at `text-sm text-muted-foreground`. Action button: `<Button variant="outline">`.
581
- - **"Create a sidebar menu item"**: `group flex items-center gap-2 rounded-md px-3 py-1.5 text-xs text-sidebar-foreground hover:text-white hover:bg-muted/60 transition-colors`. Icon: `w-4 h-4 shrink-0 text-muted-foreground transition-colors group-hover:text-white`.
582
- - **"Create a dropdown menu"**: Use `DropdownMenu` + `DropdownMenuTrigger` + `DropdownMenuContent` + `DropdownMenuItem` from `src/components/ui/dropdown-menu.tsx`. Icons auto-styled grey → white on hover via the component's built-in `[&_svg]` selectors.
583
- - **"Create a form field"**: Label at `text-sm font-medium text-foreground mb-1.5`. Use `<Input>` component (never inline raw `<input>` with custom focus styles). Help text at `text-xs text-muted-foreground mt-1`.
584
- - **"Create a tooltip"**: `bg-muted text-foreground text-xs rounded-md px-2 py-1 shadow-md`. For pill-style: use `rounded-full` instead of `rounded-md`.
585
- - **"Create a status badge"**: `inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium`. Success: `bg-success/10 text-success-foreground`. Error: `bg-destructive/10 text-destructive`.
586
-
587
- ### Iteration Guide
588
-
589
- 1. **Always use semantic color tokens** — never raw palette colors (`stone-*`, `gray-*`, `slate-*`). Every color should trace back to a `--css-variable`.
590
- 2. **`text-sm` is the default** — don't reach for `text-base` unless the context genuinely needs larger text (e.g., chat messages, hero content).
591
- 3. **`rounded-md` for elements, `rounded-lg` for containers** — this is the consistent radius hierarchy. Dialogs get `rounded-xl` or `rounded-modal`.
592
- 4. **`gap-2` is the standard** — 8px between items in any flex/grid layout. Use `gap-4` for major sections.
593
- 5. **Icons are always `text-muted-foreground`** by default and brighten to `text-foreground` or `text-white` on hover via `group` + `group-hover:`.
594
- 6. **`transition-colors duration-200`** is the standard animation. Don't add `transition-all` unless multiple property types are actually changing.
595
- 7. **`hover:bg-muted/60`** is the canonical hover background. Use it consistently across menus, nav items, and interactive rows.
596
- 8. **The `Button` component handles its own variants** — don't rebuild button styles from scratch. Use `variant="outline"` for most secondary actions.
597
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
OpenHands-Design/NORMALIZATION_LOG.md DELETED
@@ -1,36 +0,0 @@
1
- # Token Normalization Log
2
-
3
- Tracking the migration from raw Tailwind palette classes and arbitrary values to semantic design tokens.
4
-
5
- ## Completed
6
-
7
- - [x] `.dark` CSS block now declares all variables from `:root` (`--modal-background`, `--radius-*`, `--success`, `--warning`, `--info`, `--gradient-*`, `--shadow-*`, `--font-*`, `--settings-*`)
8
- - [x] Migrated ~160 raw `stone-*` utility classes across 17 component files to semantic tokens (excluding `themeAppClassMap.ts`, `index.ts`, and scrollbar utilities)
9
- - [x] Migrated ~44 raw `gray-*` utility classes across 10 files to semantic tokens
10
- - [x] Migrated ~83 raw `neutral-*` classes across 3 files to semantic tokens (preserving VS Code diff mock borders)
11
- - [x] Replaced 47 arbitrary border radius values (`rounded-[6px]` → `rounded-lg`, `rounded-[100px]` → `rounded-full`, `rounded-[12px]` → `rounded-xl`, `rounded-[4px]` → `rounded-md`, `rounded-r-[100px]` → `rounded-r-full`)
12
- - [x] Replaced 74 arbitrary font sizes (`text-[11px]` → `text-xs`, `text-[10px]` → `text-xs`, `text-[12px]` → `text-xs`)
13
- - [x] Standardized 57 `hover:bg-muted` opacity variants (`/40`, `/50`, `/70`, `/80`) to canonical `/60`
14
- - [x] Replaced 6 unsafe `text-white` usages with semantic tokens (`text-foreground`, `text-card-foreground`)
15
- - [x] Replaced `bg-[#141414]` → `bg-secondary` (2 instances)
16
- - [x] Migrated ~100+ chromatic palette classes to semantic tokens: `amber/yellow` → `warning`, `blue/sky` → `info`, `green/emerald` → `success`, `red` → `destructive`
17
- - [x] Unified tooltip backgrounds from `bg-popover`/`bg-card` to `bg-muted` across all 6 tooltip instances
18
- - [x] Fixed 33 inline white buttons (`bg-white text-black hover:bg-muted/60` → `bg-primary text-primary-foreground hover:bg-primary/85`)
19
- - [x] Fixed Button `light` variant from `bg-white text-black hover:bg-zinc-200` to `bg-primary text-primary-foreground hover:bg-primary/85`
20
- - [x] Removed Dialog `--ring` inline override (`0 0% 95%`) that caused inconsistent focus ring color in modals
21
- - [x] Updated global `--ring` from `0 0% 50%` to `0 0% 80%` for better visibility
22
- - [x] Normalized ~50 inline input/textarea/select focus styles to canonical `focus-visible:` pattern (from mixed `focus:`/`focus-visible:` with missing offsets)
23
- - [x] Changed all focus rings from `ring-2` to `ring-1` site-wide (~97 instances across 39 files)
24
- - [x] Added `appearance: none` on `input[type="search"]` to strip browser default focus chrome
25
- - [x] Removed focus ring from dialog close button
26
- - [x] Consolidated `active:scale-95` (3 uses) → `active:scale-[0.97]` to match Button standard
27
- - [x] Migrated `ChatThread.tsx` `messageTypeColors`: `yellow-500` → `warning`, `blue-500` → `info` (3 categories). Remaining categorical colors (`orange-500`, `indigo-500`, `purple-500`, `pink-500`) kept as raw palette — no semantic equivalent for multi-category distinctness
28
- - [x] Migrated 14 remaining `bg-white` usages in non-button contexts to semantic tokens: toggles → `bg-primary`, resize grips → `bg-foreground`, badge → `bg-primary`, attachment previews → `bg-foreground/5`, CTA buttons → `bg-primary text-primary-foreground hover:bg-primary/85`, ghost hover buttons → `hover:bg-primary hover:text-primary-foreground`
29
- - [x] Documented `duration-200` vs `duration-300` convention (200ms = local feedback, 300ms = layout/panel motion)
30
- - [x] ~~Migrate legacy `index.ts`~~ — invalid; `screens/index.ts` and `components/workflow/index.ts` are barrel files with zero raw color classes
31
-
32
- ## Deferred by Design
33
-
34
- - [ ] `themeAppClassMap.ts` and `NewUserExperienceFlowchart.tsx` use raw `stone-*` / `rgb()` values — these are **theme definition maps** that intentionally encode per-theme palettes (dark/light/sepia). Migrating requires defining CSS variables for each theme mode, which is an architectural change
35
- - [ ] `sepia` theme in `themeAppClassMap.ts` uses hardcoded `rgb()` — requires defining a `.theme-sepia` CSS variable block before semantic classes can replace arbitrary values
36
- - [ ] `ChatThread.tsx` categorical palette (`orange-500`, `indigo-500`, `purple-500`, `pink-500`) for `bug`, `docs`, `dependency`, `git` message types — no semantic tokens exist for multi-category distinctness; would need new `--chart-*` or `--category-*` CSS variables
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
OpenHands-Design/README.md DELETED
@@ -1,222 +0,0 @@
1
- # OpenHands Design System
2
-
3
- A portable design system extracted from the OpenHands UI. Drop it into any React + Tailwind project to get a consistent dark-first interface with semantic color tokens, pre-built components, and a comprehensive style guide.
4
-
5
- ## What's Included
6
-
7
- ```
8
- OpenHands-Design/
9
- DESIGN.md # Full design system specification
10
- README.md # This file
11
- tailwind.config.js # Tailwind theme (colors, radii, fonts, animations)
12
- src/
13
- globals.css # CSS custom properties (design tokens) + base resets
14
- lib/
15
- utils.ts # cn() helper (clsx + tailwind-merge)
16
- components/ui/
17
- button.tsx # Button with 8 variants (default, destructive, outline, light, secondary, muted, ghost, link)
18
- input.tsx # Text input with unified focus style
19
- search-input.tsx # Search input with icon, clear button, and 3 sizes
20
- native-select.tsx # Native <select> with consistent styling
21
- ```
22
-
23
- ## Quick Start
24
-
25
- ### Install with npx
26
-
27
- From your project root:
28
-
29
- ```bash
30
- npx openhands-design
31
- ```
32
-
33
- This adds `./OpenHands-Design/` (including `DESIGN.md`, tokens, and UI components). Then ask your AI assistant to use **`DESIGN.md`** for UI work. If the folder already exists, run `npx openhands-design --force` to replace it.
34
-
35
- ### 1. Install dependencies
36
-
37
- ```bash
38
- npm install clsx tailwind-merge class-variance-authority @radix-ui/react-slot lucide-react tailwindcss-animate
39
- ```
40
-
41
- ### 2. Copy files into your project
42
-
43
- ```bash
44
- # Copy the design tokens and global CSS
45
- cp OpenHands-Design/src/globals.css your-project/src/globals.css
46
-
47
- # Copy the Tailwind config (or merge into your existing one)
48
- cp OpenHands-Design/tailwind.config.js your-project/tailwind.config.js
49
-
50
- # Copy the utility helper
51
- cp OpenHands-Design/src/lib/utils.ts your-project/src/lib/utils.ts
52
-
53
- # Copy the UI components
54
- cp -r OpenHands-Design/src/components/ui/ your-project/src/components/ui/
55
- ```
56
-
57
- ### 3. Import globals.css
58
-
59
- In your app entry point (e.g., `main.tsx` or `App.tsx`):
60
-
61
- ```tsx
62
- import './globals.css';
63
- ```
64
-
65
- ### 4. Add the dark class
66
-
67
- The system is dark-first. Add the `dark` class to your `<html>` tag:
68
-
69
- ```html
70
- <html lang="en" class="dark">
71
- ```
72
-
73
- ### 5. Start using components
74
-
75
- ```tsx
76
- import { Button } from './components/ui/button';
77
- import { Input } from './components/ui/input';
78
- import { SearchInput } from './components/ui/search-input';
79
- import { NativeSelect } from './components/ui/native-select';
80
-
81
- function Example() {
82
- return (
83
- <div className="flex flex-col gap-4 bg-background p-6 text-foreground">
84
- <h1 className="text-2xl font-semibold">Settings</h1>
85
- <p className="text-sm text-muted-foreground">Manage your account.</p>
86
-
87
- <Input placeholder="Your name" />
88
-
89
- <NativeSelect>
90
- <option>Option A</option>
91
- <option>Option B</option>
92
- </NativeSelect>
93
-
94
- <div className="flex gap-2">
95
- <Button>Save</Button>
96
- <Button variant="outline">Cancel</Button>
97
- <Button variant="destructive">Delete</Button>
98
- </div>
99
- </div>
100
- );
101
- }
102
- ```
103
-
104
- ## Using with AI Agents (Cursor, Copilot, etc.)
105
-
106
- The `DESIGN.md` file is structured as an AI-readable specification. Two ways to use it:
107
-
108
- ### Option A: Cursor Rule (recommended)
109
-
110
- Create `.cursor/rules/design-system.md` in your project:
111
-
112
- ```markdown
113
- When building UI components, follow the design system in /DESIGN.md.
114
-
115
- Key rules:
116
- - Use semantic color tokens (bg-card, text-foreground, border-border) — never raw palette classes
117
- - Use the Button, Input, SearchInput, and NativeSelect components — never raw HTML with inline styles
118
- - Hover on dark surfaces: hover:bg-muted/60
119
- - Hover on white/primary buttons: hover:bg-primary/85
120
- - Focus rings: focus-visible:ring-1 (keyboard-only, 1px)
121
- - Default text: text-sm font-normal text-foreground
122
- - Secondary text: text-sm text-muted-foreground
123
- - Standard gap: gap-2 (8px)
124
- - Standard card: bg-card border border-border rounded-lg p-4
125
- ```
126
-
127
- Every Cursor conversation will now follow your design system automatically.
128
-
129
- ### Option B: Direct prompt
130
-
131
- Paste this at the start of a conversation:
132
-
133
- > Build this feature following the design system in DESIGN.md. Use semantic tokens for all colors, the Button component for actions, and the Input component for form fields.
134
-
135
- ## Token Architecture
136
-
137
- All colors are HSL triplets stored as CSS custom properties. Tailwind maps them via `hsl(var(--token))`.
138
-
139
- ```
140
- Background scale (darkest → lightest):
141
- --background 5% #0d0d0d Page background
142
- --card 7% #121212 Cards, elevated surfaces
143
- --secondary 8% #141414 Secondary surfaces
144
- --muted 12% #1f1f1f Hover fills, badges, tooltips
145
- --border 14% #242424 Borders, dividers
146
- --muted-hover 18% #2e2e2e Hover on muted surfaces
147
-
148
- Text scale:
149
- --foreground 98% #fafafa Primary text
150
- --muted-foreground 55% #8c8c8c Secondary text, placeholders
151
- --primary 100% #ffffff Maximum emphasis, button bg
152
- --primary-foreground 0% #000000 Text on white buttons
153
-
154
- Semantic colors:
155
- --success hsl(142 71% 45%) Green — success states
156
- --warning hsl(38 92% 50%) Amber — warnings, in-progress
157
- --info hsl(217 91% 60%) Blue — links, informational
158
- --destructive hsl(0 72% 51%) Red — errors, danger
159
- ```
160
-
161
- ## Button Variants
162
-
163
- | Variant | Look | Use |
164
- |---------|------|-----|
165
- | `default` | White bg, black text | Primary CTA |
166
- | `destructive` | Red bg, white text | Delete, danger |
167
- | `outline` | Transparent, border | Secondary actions |
168
- | `light` | White bg, border | High-contrast primary |
169
- | `secondary` | Dark bg | Tertiary actions |
170
- | `muted` | Muted bg, grey text | Subdued actions |
171
- | `ghost` | Transparent, no border | Minimal chrome |
172
- | `link` | Underline on hover | Inline links |
173
-
174
- ## Customization
175
-
176
- ### Changing the color scheme
177
-
178
- Edit the HSL values in `globals.css`. Every UI element updates automatically:
179
-
180
- ```css
181
- :root {
182
- --background: 220 20% 5%; /* Add a blue tint */
183
- --card: 220 15% 8%;
184
- --border: 220 10% 16%;
185
- }
186
- ```
187
-
188
- ### Adding a light theme
189
-
190
- Create a new class block in `globals.css` with inverted values:
191
-
192
- ```css
193
- .light {
194
- --background: 0 0% 100%;
195
- --foreground: 0 0% 5%;
196
- --card: 0 0% 97%;
197
- --border: 0 0% 88%;
198
- /* ... */
199
- }
200
- ```
201
-
202
- Then toggle `class="light"` on the `<html>` element.
203
-
204
- ## Reference
205
-
206
- See [DESIGN.md](./DESIGN.md) for the complete specification including:
207
-
208
- 1. Visual theme and atmosphere
209
- 2. Full color palette with hex values
210
- 3. Typography rules and type scale
211
- 4. Component styling recipes
212
- 5. Layout principles and spacing system
213
- 6. Depth and elevation system
214
- 7. Do's and Don'ts
215
- 8. Responsive behavior
216
- 9. Interaction and motion patterns
217
- 10. AI agent prompt guide
218
- 11. Normalization backlog
219
-
220
- ## License
221
-
222
- MIT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
OpenHands-Design/index.html DELETED
@@ -1,396 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>OpenHands Design System</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
9
- <style>
10
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
11
-
12
- :root{
13
- --bg:hsl(0 0% 5%);
14
- --card:hsl(0 0% 7%);
15
- --secondary:hsl(0 0% 8%);
16
- --muted:hsl(0 0% 12%);
17
- --border:hsl(0 0% 14%);
18
- /* DESIGN.md: interactive border emphasis (e.g. card/button hover) */
19
- --border-hover:hsl(0 0% 24%);
20
- --muted-hover:hsl(0 0% 18%);
21
- --fg:hsl(0 0% 98%);
22
- --fg-muted:hsl(0 0% 55%);
23
- --primary:hsl(0 0% 100%);
24
- --primary-fg:hsl(0 0% 0%);
25
- --success:#22c55e;
26
- --success-fg:#86efac;
27
- --warning:#f59e0b;
28
- --info:#3b82f6;
29
- --destructive:#dc2626;
30
- --destructive-fg:#fafafa;
31
- --ring:#cccccc;
32
- --shadow-card:0 1px 2px 0 hsl(0 0% 0% / 0.3);
33
- --font-sans:'Inter',system-ui,sans-serif;
34
- --font-mono:'JetBrains Mono',monospace;
35
- }
36
-
37
- html{scroll-behavior:smooth}
38
- body{font-family:var(--font-sans);background:var(--bg);color:var(--fg);line-height:1.6;-webkit-font-smoothing:antialiased}
39
-
40
- /* Nav */
41
- .nav{position:sticky;top:0;z-index:50;display:flex;align-items:center;flex-wrap:nowrap;gap:16px;padding:0 24px 0 16px;height:56px;background:hsl(0 0% 5% / 0.85);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);overflow:hidden}
42
- .nav-start{display:flex;align-items:center;flex-wrap:nowrap;gap:12px;flex-shrink:0;min-width:0}
43
- .nav-github{display:inline-flex;align-items:center;gap:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--fg-muted);text-decoration:none;padding:4px 10px;border-radius:6px;border:1px solid var(--border);transition:all 0.2s;flex-shrink:0}
44
- .nav-github:hover{background:var(--muted);color:var(--fg);border-color:var(--border-hover)}
45
- .nav-links{display:flex;flex:1;justify-content:flex-end;gap:4px;list-style:none;margin:0;padding:0;min-width:0;flex-wrap:nowrap}
46
- .nav-links a{font-size:13px;color:var(--fg-muted);text-decoration:none;padding:6px 12px;border-radius:6px;transition:all 0.2s}
47
- .nav-links a:hover{color:var(--fg);background:var(--muted)}
48
- .nav-logo{display:flex;align-items:center;flex-shrink:0;opacity:0.85;transition:opacity 0.2s}
49
- .nav-logo:hover{opacity:1}
50
- .nav-logo svg{height:22px;width:auto;display:block}
51
- /* Hero */
52
- .hero{text-align:center;padding:80px 24px 64px;max-width:720px;margin:0 auto}
53
- .hero h1{font-size:48px;font-weight:600;line-height:1.1;letter-spacing:-1.5px;margin-bottom:16px}
54
- .hero p{font-size:16px;color:var(--fg-muted);line-height:1.6;max-width:520px;margin:0 auto 32px}
55
- .hero-buttons{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
56
-
57
- /* Buttons */
58
- .btn-primary{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 20px;font-size:14px;font-weight:400;border-radius:6px;text-decoration:none;transition:all 0.2s;background:var(--secondary);color:var(--fg);border:1px solid var(--border)}
59
- .btn-primary:hover{background:var(--muted-hover);border-color:var(--border-hover)}
60
- .btn-dark{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 20px;font-size:14px;font-weight:400;border-radius:6px;text-decoration:none;transition:all 0.2s;background:var(--primary);color:var(--primary-fg)}
61
- .btn-dark:hover{opacity:0.85}
62
- .btn-ghost{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 20px;font-size:14px;font-weight:400;border-radius:6px;text-decoration:none;transition:all 0.2s;color:var(--fg-muted);background:transparent}
63
- .btn-ghost:hover{color:var(--fg);background:var(--muted)}
64
- .btn-destructive{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 20px;font-size:14px;font-weight:400;border-radius:6px;text-decoration:none;transition:all 0.2s;background:var(--destructive);color:var(--destructive-fg)}
65
- .btn-destructive:hover{opacity:0.85}
66
- .btn-outline{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 20px;font-size:14px;font-weight:400;border-radius:6px;text-decoration:none;transition:all 0.2s;color:var(--fg);background:var(--bg);border:1px solid var(--border)}
67
- .btn-outline:hover{background:var(--muted);color:var(--fg)}
68
- .btn-muted{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 20px;font-size:14px;font-weight:400;border-radius:6px;text-decoration:none;transition:all 0.2s;color:var(--fg-muted);background:var(--muted)}
69
- .btn-muted:hover{background:var(--muted-hover);color:var(--fg)}
70
- .btn-pill{display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 12px;font-size:12px;font-weight:500;border-radius:9999px;text-decoration:none;transition:all 0.2s;background:var(--muted);color:var(--fg-muted)}
71
- .btn-pill:hover{color:var(--fg);background:var(--muted-hover)}
72
-
73
- /* Sections */
74
- .section{max-width:1100px;margin:0 auto;padding:64px 24px}
75
- .section-label{font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:1.5px;color:var(--fg-muted);margin-bottom:8px}
76
- .section-title{font-size:28px;font-weight:600;letter-spacing:-0.5px;margin-bottom:40px}
77
- .section-divider{border:none;border-top:1px solid var(--border);margin:0}
78
-
79
- /* Color swatches */
80
- .color-group-label{font-size:13px;font-weight:500;color:var(--fg-muted);margin-bottom:12px;margin-top:32px}
81
- .color-group-label:first-of-type{margin-top:0}
82
- .color-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;margin-bottom:24px}
83
- .color-swatch{border-radius:10px;overflow:hidden;border:1px solid var(--border);background:var(--card);transition:transform 0.2s}
84
- .color-swatch:hover{transform:translateY(-2px)}
85
- .color-swatch-block{height:80px}
86
- .color-swatch-info{padding:10px 12px}
87
- .color-swatch-name{font-size:13px;font-weight:500;margin-bottom:2px}
88
- .color-swatch-hex{font-family:var(--font-mono);font-size:11px;color:var(--fg-muted)}
89
- .color-swatch-role{font-size:11px;color:var(--fg-muted);margin-top:4px}
90
-
91
- /* Typography */
92
- .type-sample{padding:20px 0;border-bottom:1px solid var(--border)}
93
- .type-sample:last-child{border-bottom:none}
94
- .type-meta{font-size:12px;color:var(--fg-muted);margin-top:8px;font-family:var(--font-mono)}
95
-
96
- /* Buttons section */
97
- .button-row{display:flex;flex-wrap:wrap;gap:24px;align-items:flex-start}
98
- .button-item{display:flex;flex-direction:column;align-items:center;gap:8px}
99
- .button-label{font-size:11px;color:var(--fg-muted);text-align:center}
100
-
101
- /* Cards */
102
- .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}
103
- .card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:24px;transition:all 0.2s}
104
- .card:hover{border-color:var(--border-hover)}
105
- .card h3{font-size:16px;font-weight:600;margin-bottom:8px}
106
- .card p{font-size:14px;color:var(--fg-muted);line-height:1.6}
107
- .card-badge{display:inline-block;font-size:11px;font-weight:500;padding:2px 8px;border-radius:9999px;margin-bottom:12px}
108
-
109
- /* Forms */
110
- .form-group{max-width:480px;margin-bottom:24px}
111
- .form-label{display:block;font-size:14px;font-weight:500;margin-bottom:6px}
112
- .form-input,.form-textarea{width:100%;height:40px;padding:0 12px;font-size:14px;font-family:var(--font-sans);color:var(--fg);background:hsl(0 0% 12% / 0.4);border:1px solid var(--border);border-radius:6px;outline:none;transition:all 0.2s}
113
- .form-input::placeholder,.form-textarea::placeholder{color:var(--fg-muted)}
114
- .form-input:hover,.form-textarea:hover{background:hsl(0 0% 12% / 0.6)}
115
- .form-input:focus,.form-textarea:focus{background:hsl(0 0% 12% / 0.6);box-shadow:0 0 0 1px var(--ring),0 0 0 3px var(--bg)}
116
- .form-input--focus{background:hsl(0 0% 12% / 0.6) !important;box-shadow:0 0 0 1px var(--ring),0 0 0 3px var(--bg) !important}
117
- .form-input--error{border-color:var(--destructive) !important;box-shadow:0 0 0 1px var(--destructive),0 0 0 3px var(--bg) !important}
118
- .form-textarea{height:auto;min-height:80px;padding:10px 12px;resize:vertical}
119
- .form-state-label{font-size:11px;color:var(--fg-muted);margin-top:6px}
120
-
121
- /* Spacing */
122
- .spacing-row{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end}
123
- .spacing-item{display:flex;flex-direction:column;align-items:center;gap:8px}
124
- .spacing-block{height:40px;background:var(--primary);border-radius:3px;min-width:2px}
125
- .spacing-value{font-family:var(--font-mono);font-size:11px;color:var(--fg-muted)}
126
-
127
- /* Radius */
128
- .radius-row{display:flex;flex-wrap:wrap;gap:24px;align-items:flex-start}
129
- .radius-item{display:flex;flex-direction:column;align-items:center;gap:8px}
130
- .radius-box{width:64px;height:64px;border:2px solid var(--fg-muted);background:var(--card)}
131
- .radius-label{font-family:var(--font-mono);font-size:12px;color:var(--fg)}
132
- .radius-context{font-size:11px;color:var(--fg-muted)}
133
-
134
- /* Elevation */
135
- .elevation-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}
136
- .elevation-card{background:var(--card);border-radius:10px;padding:24px;min-height:100px;display:flex;flex-direction:column;justify-content:flex-end}
137
- .elevation-label{font-size:13px;font-weight:500;margin-bottom:4px}
138
- .elevation-desc{font-size:12px;color:var(--fg-muted)}
139
-
140
- /* Footer */
141
- .footer{text-align:center;padding:48px 24px;font-size:13px;color:var(--fg-muted);border-top:1px solid var(--border)}
142
- .footer a{color:var(--fg-muted);text-decoration:none;transition:color 0.2s}
143
- .footer a:hover{color:var(--fg)}
144
-
145
- /* Responsive */
146
- @media(max-width:768px){
147
- .nav-links{display:none}
148
- .hero h1{font-size:32px}
149
- .color-grid{grid-template-columns:repeat(2,1fr)}
150
- .card-grid{grid-template-columns:1fr}
151
- }
152
- </style>
153
- </head>
154
- <body>
155
-
156
- <nav class="nav">
157
- <div class="nav-start">
158
- <a class="nav-logo" href="https://github.com/FraterCCCLXIII/OpenHands-Design.md" target="_blank" rel="noopener noreferrer" aria-label="OpenHands">
159
- <svg viewBox="0 0 599.09 99.17" fill="#fff" aria-hidden="true"><path d="M159.74,53.71c0-18.39,11.81-31.21,28.17-31.21s28.17,12.82,28.17,31.21-11.81,31.21-28.17,31.21-28.17-12.82-28.17-31.21ZM205.29,53.71c0-13.16-7.17-21.68-17.38-21.68s-17.38,8.52-17.38,21.68,7.17,21.68,17.38,21.68,17.38-8.52,17.38-21.68Z"/><path d="M246.07,84.92c-5.74,0-9.95-2.28-12.74-5.57v19.82h-10.12v-59.47h10.12v4.72c2.78-3.29,7-5.57,12.74-5.57,12.4,0,19.48,10.46,19.48,23.03s-7.09,23.03-19.48,23.03ZM233.08,60.63v2.61c0,8.18,4.72,12.82,10.97,12.82,7.34,0,11.3-5.74,11.3-14.17s-3.96-14.17-11.3-14.17c-6.24,0-10.97,4.56-10.97,12.91Z"/><path d="M291.26,84.92c-12.65,0-21.51-9.36-21.51-23.03s8.77-23.03,21.09-23.03,19.65,9.7,19.65,21.85v3.37h-31.04c.76,7.59,5.32,12.23,11.81,12.23,4.98,0,8.94-2.53,10.29-7.09l8.69,3.29c-3.12,7.76-10.12,12.4-18.98,12.4ZM290.76,47.38c-5.23,0-9.28,3.12-10.8,9.11h20.33c-.08-4.89-3.12-9.11-9.53-9.11Z"/><path d="M317.25,83.99v-44.29h10.12v4.72c2.53-2.95,6.49-5.57,12.23-5.57,9.28,0,14.85,6.41,14.85,15.94v29.19h-10.12v-26.23c0-5.48-2.19-9.45-7.76-9.45-4.55,0-9.19,3.37-9.19,9.7v25.98h-10.12Z"/><path d="M404.17,23.43h10.8v60.57h-10.8v-26.49h-29.02v26.49h-10.8V23.43h10.8v24.63h29.02v-24.63Z"/><path d="M436.93,84.75c-8.43,0-14.93-5.15-14.93-13.07,0-8.44,6.33-12.15,14.85-13.92l12.23-2.53v-.76c0-4.22-2.19-6.83-7.59-6.83-4.81,0-7.34,2.19-8.52,6.5l-9.53-2.19c2.19-7.34,8.69-13.07,18.47-13.07,10.63,0,17.04,5.06,17.04,15.27v19.06c0,2.53,1.1,3.29,3.88,2.95v7.84c-7.34.84-11.22-.59-12.74-4.22-2.78,3.12-7.42,4.98-13.16,4.98ZM449.08,68.39v-5.4l-9.53,2.02c-4.3.93-7.51,2.28-7.51,6.24,0,3.46,2.53,5.4,6.41,5.4,5.4,0,10.63-2.87,10.63-8.27Z"/><path d="M469.17,83.99v-44.29h10.12v4.72c2.53-2.95,6.49-5.57,12.23-5.57,9.28,0,14.85,6.41,14.85,15.94v29.19h-10.12v-26.23c0-5.48-2.19-9.45-7.76-9.45-4.56,0-9.2,3.37-9.2,9.7v25.98h-10.12Z"/><path d="M532.56,84.92c-12.4,0-19.48-10.46-19.48-23.03s7.08-23.03,19.48-23.03c5.74,0,9.95,2.28,12.74,5.57v-21h10.12v60.57h-10.12v-4.64c-2.78,3.29-7,5.57-12.74,5.57ZM545.55,60.63c0-8.35-4.72-12.91-10.96-12.91-7.34,0-11.3,5.74-11.3,14.17s3.96,14.17,11.3,14.17c6.24,0,10.96-4.64,10.96-12.82v-2.61Z"/><path d="M560.54,75.56l7.59-6.07c2.62,4.3,7.68,7.17,12.82,7.17,4.3,0,8.27-1.52,8.27-5.48s-3.71-4.22-10.71-5.65c-7-1.43-15.01-3.21-15.01-12.65,0-8.1,7.09-14,17.29-14,7.76,0,14.68,3.46,17.88,8.35l-6.83,6.16c-2.53-3.96-6.75-6.24-11.64-6.24-4.13,0-6.83,1.86-6.83,4.81,0,3.21,3.2,3.8,8.77,4.98,7.51,1.6,16.95,3.21,16.95,13.33,0,8.94-8.18,14.68-18.22,14.68-8.18,0-16.36-3.29-20.33-9.36Z"/><path d="M64.97,14.8V1.93c0-1.07.86-1.93,1.93-1.93s1.93.86,1.93,1.93v12.87c0,1.07-.86,1.93-1.93,1.93s-1.93-.86-1.93-1.93Z"/><path d="M74.95,16.72l6.43-11.15c.53-.92,1.71-1.24,2.64-.71.92.53,1.24,1.71.71,2.64l-6.43,11.15c-.53.92-1.71,1.24-2.64.71-.92-.53-1.24-1.71-.71-2.64Z"/><path d="M58.85,16.72l-6.43-11.15c-.53-.92-1.71-1.24-2.64-.71-.92.53-1.24,1.71-.71,2.64l6.43,11.15c.53.92,1.71,1.24,2.64.71.92-.53,1.24-1.71.71-2.64Z"/><path d="M128.77,56.65c0-3.35.9-13.3,1.19-16.58.19-2.22-.07-3.44-.43-4.06-.26-.46-.67-.78-1.66-.84-.71-.05-1.49.16-2.07.68-.54.49-1.15,1.48-1.15,3.47v.11s-.89,15.12-.89,15.12c-.03.54-.29,1.05-.72,1.39-.42.34-.97.49-1.51.4l-9.29-1.47-10.02-1.33c-.93-.12-1.63-.89-1.67-1.82l-.55-11.95v-.1c-.25-4.76-.49-9.1-.49-10.44,0-3.75-.63-5.33-1.19-5.99-.44-.53-1.08-.76-2.44-.76-.49,0-.83.1-1.09.25-.25.15-.54.41-.82.94-.59,1.12-1.02,3.22-.86,6.88.21,4.76.53,8.31.85,11.51.32,3.2.63,6.1.81,9.47.27,5.28.25,8.92.03,11.39-.11,1.23-.27,2.23-.48,3.02-.2.75-.51,1.51-1.04,2.07-.64.69-1.56,1.02-2.52.79-.76-.18-1.29-.66-1.58-.97-.61-.64-1.04-1.46-1.21-1.89-.98-2.47-4.01-8.22-8.12-11.46-1.2-.95-2.07-1.22-2.62-1.26-.52-.04-.89.11-1.19.35-.33.26-.57.63-.69.99-.04.13-.06.22-.07.27,1.11,1.88,5.53,8.77,7.61,15.76,1.55,5.21,5.29,10.52,8.09,12.8,2.71,2.2,7.57,3.57,13.05,3.84,5.42.27,11.01-.57,14.95-2.33,7.6-3.41,9.14-10.91,9.84-14.16.54-2.52.55-5.22.4-7.72-.07-1.25-.18-2.41-.27-3.49-.09-1.04-.17-2.05-.17-2.88ZM110.59,24.28c0-1.17-.31-2.21-.83-2.91-.47-.63-1.16-1.07-2.26-1.07-.91,0-1.52.11-1.94.29-.39.16-.71.42-1,.9-.68,1.1-1.18,3.3-1.18,7.69l.48,10.39c.18,3.47.37,7.22.49,10.35l6.25.83v-26.47ZM114.45,51.31l5.58.88.76-12.93v-9.97c0-1.37-.56-2.21-1.22-2.74-.74-.6-1.6-.81-2-.81-.74,0-1.5.11-2.05.5-.42.3-1.07,1.01-1.07,3.05v22.01ZM124.65,32c1.15-.58,2.39-.76,3.48-.69,1.97.13,3.71.96,4.75,2.77.95,1.65,1.15,3.83.93,6.31-.3,3.43-1.18,13.11-1.18,16.25,0,.63.06,1.47.16,2.54.09,1.05.21,2.28.28,3.6.15,2.63.16,5.72-.48,8.75-.67,3.15-2.49,12.6-12.03,16.88-4.64,2.08-10.87,2.95-16.72,2.66-5.79-.28-11.64-1.73-15.29-4.7-3.44-2.8-7.59-8.79-9.35-14.69-1.99-6.67-6.29-13.24-7.36-15.11-.63-1.1-.43-2.4-.14-3.27.33-.98.98-2,1.94-2.77,1-.79,2.32-1.29,3.88-1.18,1.53.12,3.11.81,4.72,2.08,4.14,3.27,7.18,8.43,8.67,11.59.02-.15.03-.3.05-.46.19-2.21.23-5.65-.04-10.86-.17-3.26-.47-6.05-.79-9.29-.32-3.24-.65-6.87-.87-11.72-.17-3.88.23-6.82,1.31-8.86.56-1.06,1.32-1.9,2.28-2.46.96-.56,2.01-.78,3.04-.78,1.53,0,3.43.22,4.95,1.66.13-.29.28-.56.44-.81.7-1.13,1.63-1.93,2.77-2.42,1.1-.47,2.29-.6,3.46-.6,2.36,0,4.19,1.04,5.36,2.63.76,1.03,1.22,2.23,1.44,3.46,1.25-.57,2.51-.64,3.28-.64,1.31,0,3.02.53,4.43,1.68,1.49,1.21,2.65,3.11,2.65,5.74v2.71Z"/><path d="M5.12,56.65c0-3.35-.9-13.3-1.19-16.58-.19-2.22.07-3.44.43-4.06.26-.46.67-.78,1.66-.84.71-.05,1.49.16,2.07.68.54.49,1.15,1.48,1.15,3.47v.11s.89,15.12.89,15.12c.03.54.29,1.05.72,1.39.42.34.97.49,1.51.4l9.29-1.47,10.02-1.33c.93-.12,1.63-.89,1.67-1.82l.55-11.95v-.1c.25-4.76.48-9.1.48-10.44,0-3.75.63-5.33,1.19-5.99.44-.53,1.08-.76,2.44-.76.49,0,.83.1,1.09.25.25.15.54.41.82.94.59,1.12,1.02,3.22.86,6.88-.21,4.76-.53,8.31-.85,11.51-.32,3.2-.63,6.1-.81,9.47-.27,5.28-.25,8.92-.03,11.39.11,1.23.27,2.23.48,3.02.2.75.51,1.51,1.04,2.07.65.69,1.56,1.02,2.52.79.76-.18,1.29-.66,1.58-.97.61-.64,1.04-1.46,1.21-1.89.98-2.47,4.01-8.22,8.12-11.46,1.2-.95,2.07-1.22,2.62-1.26.52-.04.89.11,1.19.35.33.26.57.63.69.99.04.13.06.22.07.27-1.11,1.88-5.53,8.77-7.61,15.76-1.55,5.21-5.29,10.52-8.09,12.8-2.71,2.2-7.57,3.57-13.05,3.84-5.43.27-11.01-.57-14.95-2.33-7.6-3.41-9.15-10.91-9.84-14.16-.54-2.52-.55-5.22-.4-7.72.07-1.25.18-2.41.27-3.49.09-1.04.17-2.05.17-2.88ZM23.29,24.28c0-1.17.31-2.21.83-2.91.47-.63,1.16-1.07,2.26-1.07.91,0,1.52.11,1.95.29.39.16.71.42,1,.9.68,1.1,1.18,3.3,1.18,7.69l-.48,10.39c-.18,3.47-.37,7.22-.49,10.35l-6.25.83v-26.47ZM19.43,51.31l-5.58.88-.76-12.93v-9.97c0-1.37.56-2.21,1.22-2.74.74-.6,1.59-.81,2-.81.74,0,1.5.11,2.05.5.42.3,1.07,1.01,1.07,3.05v22.01ZM9.24,32c-1.15-.58-2.39-.76-3.48-.69-1.97.13-3.7.96-4.75,2.77-.95,1.65-1.15,3.83-.93,6.31.3,3.43,1.18,13.11,1.18,16.25,0,.63-.07,1.47-.16,2.54-.09,1.05-.21,2.28-.28,3.6-.15,2.63-.16,5.72.48,8.75.67,3.15,2.49,12.6,12.04,16.88,4.64,2.08,10.87,2.95,16.72,2.66,5.79-.28,11.65-1.73,15.29-4.7,3.44-2.8,7.59-8.79,9.35-14.69,1.99-6.67,6.29-13.24,7.36-15.11.63-1.1.43-2.4.14-3.27-.33-.98-.98-2-1.94-2.77-1-.79-2.32-1.29-3.88-1.18-1.53.12-3.11.81-4.72,2.08-4.14,3.27-7.18,8.43-8.67,11.59-.02-.15-.03-.3-.05-.46-.19-2.21-.23-5.65.04-10.86.17-3.26.47-6.05.79-9.29.32-3.24.65-6.87.87-11.72.17-3.88-.23-6.82-1.31-8.86-.56-1.06-1.32-1.9-2.28-2.46-.96-.56-2.01-.78-3.04-.78-1.53,0-3.43.22-4.95,1.66-.13-.29-.28-.56-.44-.81-.7-1.13-1.63-1.93-2.77-2.42-1.1-.47-2.28-.6-3.46-.6-2.36,0-4.19,1.04-5.36,2.63-.76,1.03-1.22,2.23-1.44,3.46-1.25-.57-2.51-.64-3.27-.64-1.31,0-3.02.53-4.43,1.68-1.49,1.21-2.64,3.11-2.64,5.74v2.71Z"/></svg>
160
- </a>
161
- <a class="nav-github" href="https://github.com/FraterCCCLXIII/OpenHands-Design.md" target="_blank" rel="noopener noreferrer" aria-label="OpenHands-Design.md on GitHub">
162
- <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2 .37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
163
- OpenHands-Design.md
164
- </a>
165
- </div>
166
- <ul class="nav-links">
167
- <li><a href="#colors">Colors</a></li>
168
- <li><a href="#typography">Typography</a></li>
169
- <li><a href="#buttons">Buttons</a></li>
170
- <li><a href="#cards">Cards</a></li>
171
- <li><a href="#forms">Forms</a></li>
172
- <li><a href="#spacing">Spacing</a></li>
173
- <li><a href="#radius">Radius</a></li>
174
- <li><a href="#elevation">Elevation</a></li>
175
- </ul>
176
- </nav>
177
-
178
- <section class="hero">
179
- <h1>OpenHands<br>Design System</h1>
180
- <p>A design token catalog generated from DESIGN.md. Every color, font, component, and spacing value, visualized on the near-black monochrome canvas.</p>
181
- <div class="hero-buttons">
182
- <a class="btn-dark" href="https://github.com/FraterCCCLXIII/OpenHands-Design.md" target="_blank">View Repository</a>
183
- <a class="btn-primary" href="#colors">Explore Tokens</a>
184
- </div>
185
- </section>
186
-
187
- <hr class="section-divider">
188
-
189
- <!-- ==================== COLORS ==================== -->
190
- <section class="section" id="colors">
191
- <div class="section-label">01 / Colors</div>
192
- <h2 class="section-title">Color Palette</h2>
193
-
194
- <div class="color-group-label">Core Surfaces</div>
195
- <div class="color-grid">
196
- <div class="color-swatch"><div class="color-swatch-block" style="background:#0d0d0d"></div><div class="color-swatch-info"><div class="color-swatch-name">background</div><div class="color-swatch-hex">#0d0d0d</div><div class="color-swatch-role">Page background, app shell</div></div></div>
197
- <div class="color-swatch"><div class="color-swatch-block" style="background:#121212"></div><div class="color-swatch-info"><div class="color-swatch-name">card</div><div class="color-swatch-hex">#121212</div><div class="color-swatch-role">Card surfaces, elevated containers</div></div></div>
198
- <div class="color-swatch"><div class="color-swatch-block" style="background:#141414"></div><div class="color-swatch-info"><div class="color-swatch-name">secondary</div><div class="color-swatch-hex">#141414</div><div class="color-swatch-role">Secondary surfaces, sidebar accent</div></div></div>
199
- <div class="color-swatch"><div class="color-swatch-block" style="background:#1f1f1f"></div><div class="color-swatch-info"><div class="color-swatch-name">muted</div><div class="color-swatch-hex">#1f1f1f</div><div class="color-swatch-role">Hover fills, badges, tooltips</div></div></div>
200
- <div class="color-swatch"><div class="color-swatch-block" style="background:#242424"></div><div class="color-swatch-info"><div class="color-swatch-name">border</div><div class="color-swatch-hex">#242424</div><div class="color-swatch-role">Borders, input borders, dividers</div></div></div>
201
- <div class="color-swatch"><div class="color-swatch-block" style="background:#2e2e2e"></div><div class="color-swatch-info"><div class="color-swatch-name">muted-hover</div><div class="color-swatch-hex">#2e2e2e</div><div class="color-swatch-role">Hover on muted surfaces</div></div></div>
202
- </div>
203
-
204
- <div class="color-group-label">Core Text</div>
205
- <div class="color-grid">
206
- <div class="color-swatch"><div class="color-swatch-block" style="background:#fafafa"></div><div class="color-swatch-info"><div class="color-swatch-name">foreground</div><div class="color-swatch-hex">#fafafa</div><div class="color-swatch-role">Primary text, headings</div></div></div>
207
- <div class="color-swatch"><div class="color-swatch-block" style="background:#8c8c8c"></div><div class="color-swatch-info"><div class="color-swatch-name">muted-foreground</div><div class="color-swatch-hex">#8c8c8c</div><div class="color-swatch-role">Secondary text, labels, placeholders</div></div></div>
208
- <div class="color-swatch"><div class="color-swatch-block" style="background:#ffffff;border-bottom:1px solid hsl(0 0% 14%)"></div><div class="color-swatch-info"><div class="color-swatch-name">primary</div><div class="color-swatch-hex">#ffffff</div><div class="color-swatch-role">Maximum emphasis, button bg</div></div></div>
209
- <div class="color-swatch"><div class="color-swatch-block" style="background:#000000"></div><div class="color-swatch-info"><div class="color-swatch-name">primary-foreground</div><div class="color-swatch-hex">#000000</div><div class="color-swatch-role">Text on white buttons</div></div></div>
210
- </div>
211
-
212
- <div class="color-group-label">Semantic / Status</div>
213
- <div class="color-grid">
214
- <div class="color-swatch"><div class="color-swatch-block" style="background:#22c55e"></div><div class="color-swatch-info"><div class="color-swatch-name">success</div><div class="color-swatch-hex">#22c55e</div><div class="color-swatch-role">Success states, running</div></div></div>
215
- <div class="color-swatch"><div class="color-swatch-block" style="background:#86efac"></div><div class="color-swatch-info"><div class="color-swatch-name">success-foreground</div><div class="color-swatch-hex">#86efac</div><div class="color-swatch-role">Success text on dark</div></div></div>
216
- <div class="color-swatch"><div class="color-swatch-block" style="background:#f59e0b"></div><div class="color-swatch-info"><div class="color-swatch-name">warning</div><div class="color-swatch-hex">#f59e0b</div><div class="color-swatch-role">Warning, caution badges</div></div></div>
217
- <div class="color-swatch"><div class="color-swatch-block" style="background:#3b82f6"></div><div class="color-swatch-info"><div class="color-swatch-name">info</div><div class="color-swatch-hex">#3b82f6</div><div class="color-swatch-role">Informational, links</div></div></div>
218
- <div class="color-swatch"><div class="color-swatch-block" style="background:#dc2626"></div><div class="color-swatch-info"><div class="color-swatch-name">destructive</div><div class="color-swatch-hex">#dc2626</div><div class="color-swatch-role">Error, danger, delete</div></div></div>
219
- <div class="color-swatch"><div class="color-swatch-block" style="background:#cccccc"></div><div class="color-swatch-info"><div class="color-swatch-name">ring</div><div class="color-swatch-hex">#cccccc</div><div class="color-swatch-role">Focus rings (1px, keyboard-only)</div></div></div>
220
- </div>
221
-
222
- <div class="color-group-label">Surface Scale (5% &rarr; 18% lightness)</div>
223
- <div style="display:flex;border-radius:10px;overflow:hidden;height:64px;border:1px solid var(--border);margin-bottom:24px">
224
- <div style="flex:1;background:#0d0d0d" title="5% — background"></div>
225
- <div style="flex:1;background:#121212" title="7% — card"></div>
226
- <div style="flex:1;background:#141414" title="8% — secondary"></div>
227
- <div style="flex:1;background:#1f1f1f" title="12% — muted"></div>
228
- <div style="flex:1;background:#242424" title="14% — border"></div>
229
- <div style="flex:1;background:#2e2e2e" title="18% — muted-hover"></div>
230
- </div>
231
- <div style="display:flex;justify-content:space-between;font-family:var(--font-mono);font-size:11px;color:var(--fg-muted);margin-bottom:8px">
232
- <span>5%</span><span>7%</span><span>8%</span><span>12%</span><span>14%</span><span>18%</span>
233
- </div>
234
- </section>
235
-
236
- <hr class="section-divider">
237
-
238
- <!-- ==================== TYPOGRAPHY ==================== -->
239
- <section class="section" id="typography">
240
- <div class="section-label">02 / Typography</div>
241
- <h2 class="section-title">Typography Scale</h2>
242
-
243
- <div class="type-sample"><div style="font-size:30px;font-weight:600;line-height:1.2;letter-spacing:-0.5px">Hero Heading (text-3xl)</div><div class="type-meta">30px / 600 / 1.2 / -0.5px / Inter</div></div>
244
- <div class="type-sample"><div style="font-size:24px;font-weight:600;line-height:1.25;letter-spacing:-0.3px">Page Heading (text-2xl)</div><div class="type-meta">24px / 600 / 1.25 / -0.3px / Inter</div></div>
245
- <div class="type-sample"><div style="font-size:20px;font-weight:600;line-height:1.3">Sub-heading (text-xl)</div><div class="type-meta">20px / 600 / 1.3 / Inter</div></div>
246
- <div class="type-sample"><div style="font-size:18px;font-weight:600;line-height:1.4">Section Title (text-lg)</div><div class="type-meta">18px / 600 / 1.4 / Inter</div></div>
247
- <div class="type-sample"><div style="font-size:16px;font-weight:400;line-height:1.5">Body Large — Larger body text for chat messages and hero content. (text-base)</div><div class="type-meta">16px / 400 / 1.5 / Inter</div></div>
248
- <div class="type-sample"><div style="font-size:14px;font-weight:400;line-height:1.5">Body — Standard UI text for labels, descriptions, and interface elements. (text-sm)</div><div class="type-meta">14px / 400 / 1.5 / Inter — primary body size (711 uses)</div></div>
249
- <div class="type-sample"><div style="font-size:14px;font-weight:500;line-height:1.5">Label — Form labels, nav items, and badges. (text-sm font-medium)</div><div class="type-meta">14px / 500 / 1.5 / Inter — label weight (304 uses)</div></div>
250
- <div class="type-sample"><div style="font-size:12px;font-weight:400;line-height:1.5;color:var(--fg-muted)">Secondary — Metadata, captions, and small labels. (text-xs)</div><div class="type-meta">12px / 400 / 1.5 / Inter — secondary size (427 uses)</div></div>
251
- <div class="type-sample"><div style="font-family:var(--font-mono);font-size:14px;font-weight:400;line-height:1.6">const design = await openHands.init({ tokens: true });</div><div class="type-meta">14px / 400 / 1.6 / JetBrains Mono — code / technical text</div></div>
252
- <div class="type-sample"><div style="font-size:11px;font-weight:500;line-height:1.27;text-transform:uppercase;letter-spacing:1.5px;color:var(--fg-muted)">SYSTEM CATEGORY</div><div class="type-meta">11px / 500 / uppercase / tracking-wide — section labels</div></div>
253
- </section>
254
-
255
- <hr class="section-divider">
256
-
257
- <!-- ==================== BUTTONS ==================== -->
258
- <section class="section" id="buttons">
259
- <div class="section-label">03 / Buttons</div>
260
- <h2 class="section-title">Button Variants</h2>
261
- <div class="button-row">
262
- <div class="button-item"><a class="btn-dark" href="#">Save Changes</a><div class="button-label">default</div></div>
263
- <div class="button-item"><a class="btn-destructive" href="#">Delete</a><div class="button-label">destructive</div></div>
264
- <div class="button-item"><a class="btn-outline" href="#">Cancel</a><div class="button-label">outline</div></div>
265
- <div class="button-item"><a class="btn-primary" href="#">Settings</a><div class="button-label">secondary</div></div>
266
- <div class="button-item"><a class="btn-muted" href="#">Archive</a><div class="button-label">muted</div></div>
267
- <div class="button-item"><a class="btn-ghost" href="#">Learn More</a><div class="button-label">ghost</div></div>
268
- <div class="button-item"><a href="#" style="font-size:14px;color:var(--primary);text-decoration:none;text-underline-offset:4px" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">View docs</a><div class="button-label">link</div></div>
269
- </div>
270
-
271
- <div style="margin-top:40px">
272
- <div class="color-group-label">Status Pills</div>
273
- <div class="button-row">
274
- <div class="button-item"><span style="display:inline-block;background:hsl(142 71% 45% / 0.1);color:var(--success-fg);padding:3px 10px;border-radius:9999px;font-size:12px;font-weight:500">Success</span><div class="button-label">success</div></div>
275
- <div class="button-item"><span style="display:inline-block;background:hsl(38 92% 50% / 0.15);color:var(--warning);padding:3px 10px;border-radius:9999px;font-size:12px;font-weight:500">Warning</span><div class="button-label">warning</div></div>
276
- <div class="button-item"><span style="display:inline-block;background:hsl(217 91% 60% / 0.15);color:var(--info);padding:3px 10px;border-radius:9999px;font-size:12px;font-weight:500">Info</span><div class="button-label">info</div></div>
277
- <div class="button-item"><span style="display:inline-block;background:hsl(0 72% 51% / 0.1);color:var(--destructive);padding:3px 10px;border-radius:9999px;font-size:12px;font-weight:500">Error</span><div class="button-label">destructive</div></div>
278
- </div>
279
- </div>
280
- </section>
281
-
282
- <hr class="section-divider">
283
-
284
- <!-- ==================== CARDS ==================== -->
285
- <section class="section" id="cards">
286
- <div class="section-label">04 / Cards</div>
287
- <h2 class="section-title">Card Examples</h2>
288
- <div class="card-grid">
289
- <div class="card">
290
- <div class="card-badge" style="background:hsl(142 71% 45% / 0.1);color:var(--success-fg)">Standard</div>
291
- <h3>Standard Card</h3>
292
- <p>bg-card border border-border rounded-lg p-4. The workhorse container for settings panels, content sections, and list items.</p>
293
- </div>
294
- <div class="card" style="box-shadow:var(--shadow-card);border-radius:12px;padding:24px">
295
- <div class="card-badge" style="background:hsl(217 91% 60% / 0.15);color:var(--info)">Elevated</div>
296
- <h3>Elevated Card</h3>
297
- <p>bg-card border border-border rounded-xl p-6 shadow-lg. For modals, dialogs, and featured content that needs to float above the surface.</p>
298
- </div>
299
- <div class="card" style="border-color:var(--border-hover)">
300
- <div class="card-badge" style="background:hsl(38 92% 50% / 0.15);color:var(--warning)">Interactive</div>
301
- <h3>Interactive Card</h3>
302
- <p>hover:border-white/30. Cards that respond to hover with a subtle border brightening to indicate they are clickable.</p>
303
- </div>
304
- </div>
305
- </section>
306
-
307
- <hr class="section-divider">
308
-
309
- <!-- ==================== FORMS ==================== -->
310
- <section class="section" id="forms">
311
- <div class="section-label">05 / Forms</div>
312
- <h2 class="section-title">Form Elements</h2>
313
- <div class="form-group"><label class="form-label">Project Name</label><input class="form-input" type="text" placeholder="my-openhands-project"><div class="form-state-label">Default (border-border, bg-muted/40)</div></div>
314
- <div class="form-group"><label class="form-label">Repository</label><input class="form-input form-input--focus" type="text" value="openhands/agent"><div class="form-state-label">Focus (ring-1, ring-ring, bg-muted/60)</div></div>
315
- <div class="form-group"><label class="form-label">API Key</label><input class="form-input form-input--error" type="text" value="invalid-key-123"><div class="form-state-label">Error (border-destructive)</div></div>
316
- <div class="form-group"><label class="form-label">Instructions</label><textarea class="form-textarea" placeholder="Describe the task for the agent..."></textarea></div>
317
- <div class="form-group">
318
- <label class="form-label">Framework</label>
319
- <div style="position:relative">
320
- <select class="form-input" style="appearance:none;padding-right:36px;cursor:pointer">
321
- <option>React + Tailwind</option>
322
- <option>Next.js</option>
323
- <option>Vue</option>
324
- </select>
325
- <svg style="position:absolute;right:12px;top:50%;transform:translateY(-50%);pointer-events:none" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
326
- </div>
327
- <div class="form-state-label">NativeSelect (appearance: none, chevron overlay)</div>
328
- </div>
329
- </section>
330
-
331
- <hr class="section-divider">
332
-
333
- <!-- ==================== SPACING ==================== -->
334
- <section class="section" id="spacing">
335
- <div class="section-label">06 / Spacing</div>
336
- <h2 class="section-title">Spacing Scale</h2>
337
- <div class="spacing-row">
338
- <div class="spacing-item"><div class="spacing-block" style="width:4px"></div><div class="spacing-value">4 (gap-1)</div></div>
339
- <div class="spacing-item"><div class="spacing-block" style="width:6px"></div><div class="spacing-value">6 (gap-1.5)</div></div>
340
- <div class="spacing-item"><div class="spacing-block" style="width:8px"></div><div class="spacing-value">8 (gap-2)</div></div>
341
- <div class="spacing-item"><div class="spacing-block" style="width:12px"></div><div class="spacing-value">12 (gap-3)</div></div>
342
- <div class="spacing-item"><div class="spacing-block" style="width:16px"></div><div class="spacing-value">16 (gap-4)</div></div>
343
- <div class="spacing-item"><div class="spacing-block" style="width:24px"></div><div class="spacing-value">24 (gap-6)</div></div>
344
- <div class="spacing-item"><div class="spacing-block" style="width:32px"></div><div class="spacing-value">32 (gap-8)</div></div>
345
- <div class="spacing-item"><div class="spacing-block" style="width:48px"></div><div class="spacing-value">48 (gap-12)</div></div>
346
- </div>
347
-
348
- <div style="margin-top:40px">
349
- <div class="color-group-label">Padding Patterns</div>
350
- <div style="display:flex;flex-wrap:wrap;gap:16px;margin-top:12px">
351
- <div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:8px 12px;font-size:12px;color:var(--fg-muted)">px-3 py-2 <span style="color:var(--fg)">compact</span></div>
352
- <div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:8px 16px;font-size:12px;color:var(--fg-muted)">px-4 py-2 <span style="color:var(--fg)">standard</span></div>
353
- <div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:16px;font-size:12px;color:var(--fg-muted)">p-4 <span style="color:var(--fg)">card</span></div>
354
- <div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:24px;font-size:12px;color:var(--fg-muted)">p-6 <span style="color:var(--fg)">dialog</span></div>
355
- </div>
356
- </div>
357
- </section>
358
-
359
- <hr class="section-divider">
360
-
361
- <!-- ==================== RADIUS ==================== -->
362
- <section class="section" id="radius">
363
- <div class="section-label">07 / Radius</div>
364
- <h2 class="section-title">Border Radius Scale</h2>
365
- <div class="radius-row">
366
- <div class="radius-item"><div class="radius-box" style="border-radius:2px"></div><div class="radius-label">2px</div><div class="radius-context">rounded-sm</div></div>
367
- <div class="radius-item"><div class="radius-box" style="border-radius:4px"></div><div class="radius-label">4px</div><div class="radius-context">rounded-md</div></div>
368
- <div class="radius-item"><div class="radius-box" style="border-radius:6px"></div><div class="radius-label">6px</div><div class="radius-context">rounded-lg</div></div>
369
- <div class="radius-item"><div class="radius-box" style="border-radius:12px"></div><div class="radius-label">12px</div><div class="radius-context">rounded-modal</div></div>
370
- <div class="radius-item"><div class="radius-box" style="border-radius:16px"></div><div class="radius-label">16px</div><div class="radius-context">rounded-2xl</div></div>
371
- <div class="radius-item"><div class="radius-box" style="border-radius:9999px"></div><div class="radius-label">9999px</div><div class="radius-context">rounded-full</div></div>
372
- </div>
373
- </section>
374
-
375
- <hr class="section-divider">
376
-
377
- <!-- ==================== ELEVATION ==================== -->
378
- <section class="section" id="elevation">
379
- <div class="section-label">08 / Elevation</div>
380
- <h2 class="section-title">Elevation &amp; Depth</h2>
381
- <div class="elevation-grid">
382
- <div class="elevation-card" style="border:1px solid var(--border)"><div class="elevation-label">Level 0: Flat</div><div class="elevation-desc">No shadow, bg-background</div></div>
383
- <div class="elevation-card" style="border:1px solid var(--border);box-shadow:0 1px 2px 0 hsl(0 0% 0% / 0.3)"><div class="elevation-label">Level 1: Surface</div><div class="elevation-desc">shadow-card + border</div></div>
384
- <div class="elevation-card" style="border:1px solid var(--border);box-shadow:0 4px 6px -1px hsl(0 0% 0% / 0.3),0 2px 4px -2px hsl(0 0% 0% / 0.3)"><div class="elevation-label">Level 2: Raised</div><div class="elevation-desc">shadow-md — dropdowns, popovers</div></div>
385
- <div class="elevation-card" style="border:1px solid var(--border);box-shadow:0 10px 15px -3px hsl(0 0% 0% / 0.3),0 4px 6px -4px hsl(0 0% 0% / 0.3)"><div class="elevation-label">Level 3: Floating</div><div class="elevation-desc">shadow-lg — modals, dialogs</div></div>
386
- <div class="elevation-card" style="border:1px solid var(--border);box-shadow:0 20px 25px -5px hsl(0 0% 0% / 0.3),0 8px 10px -6px hsl(0 0% 0% / 0.3)"><div class="elevation-label">Level 4: Overlay</div><div class="elevation-desc">shadow-xl — full-screen overlays</div></div>
387
- <div class="elevation-card" style="border:1px solid var(--border);box-shadow:0 0 0 1px var(--ring),0 0 0 3px var(--bg)"><div class="elevation-label">Focus Ring</div><div class="elevation-desc">ring-1 ring-ring ring-offset-2</div></div>
388
- </div>
389
- </section>
390
-
391
- <footer class="footer">
392
- <a href="https://github.com/FraterCCCLXIII/OpenHands-Design.md" target="_blank" rel="noopener noreferrer">OpenHands-Design.md</a> &mdash; Design tokens for the OpenHands UI
393
- </footer>
394
-
395
- </body>
396
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
OpenHands-Design/src/components/ui/button.tsx DELETED
@@ -1,50 +0,0 @@
1
- import * as React from 'react';
2
- import { Slot } from '@radix-ui/react-slot';
3
- import { cva, type VariantProps } from 'class-variance-authority';
4
-
5
- import { cn } from '../../lib/utils';
6
-
7
- const buttonVariants = cva(
8
- 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-normal ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.97] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
9
- {
10
- variants: {
11
- variant: {
12
- default: 'bg-primary text-primary-foreground hover:bg-primary/85',
13
- destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/85',
14
- outline: 'border border-input bg-background hover:bg-muted hover:text-foreground',
15
- light: 'border border-input bg-primary text-primary-foreground hover:bg-primary/85',
16
- secondary: 'bg-secondary text-secondary-foreground hover:bg-muted-hover',
17
- muted: 'bg-muted text-muted-foreground hover:bg-muted-hover hover:text-foreground',
18
- ghost: 'hover:bg-muted hover:text-foreground',
19
- link: 'text-primary underline-offset-4 hover:underline',
20
- },
21
- size: {
22
- default: 'h-10 px-4 py-2',
23
- sm: 'h-10 rounded-md px-3',
24
- xs: 'h-10 rounded-md px-3 text-xs',
25
- lg: 'h-10 rounded-md px-8',
26
- icon: 'h-10 w-10',
27
- },
28
- },
29
- defaultVariants: {
30
- variant: 'default',
31
- size: 'default',
32
- },
33
- }
34
- );
35
-
36
- export interface ButtonProps
37
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
- VariantProps<typeof buttonVariants> {
39
- asChild?: boolean;
40
- }
41
-
42
- const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
- ({ className, variant, size, asChild = false, ...props }, ref) => {
44
- const Comp = asChild ? Slot : 'button';
45
- return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
46
- }
47
- );
48
- Button.displayName = 'Button';
49
-
50
- export { Button, buttonVariants };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
OpenHands-Design/src/components/ui/input.tsx DELETED
@@ -1,22 +0,0 @@
1
- import * as React from 'react';
2
-
3
- import { cn } from '../../lib/utils';
4
-
5
- const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
6
- ({ className, type, ...props }, ref) => {
7
- return (
8
- <input
9
- type={type}
10
- className={cn(
11
- 'flex h-10 w-full rounded-md border border-border bg-muted/40 px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:bg-muted/60 hover:bg-muted/60 disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted/30 md:text-sm',
12
- className
13
- )}
14
- ref={ref}
15
- {...props}
16
- />
17
- );
18
- }
19
- );
20
- Input.displayName = 'Input';
21
-
22
- export { Input };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
OpenHands-Design/src/components/ui/native-select.tsx DELETED
@@ -1,26 +0,0 @@
1
- import * as React from 'react';
2
- import { ChevronDown } from 'lucide-react';
3
-
4
- import { cn } from '../../lib/utils';
5
-
6
- const nativeSelectClassName =
7
- 'h-10 w-full appearance-none rounded-md border border-border bg-muted/40 py-2 pl-3 pr-10 text-sm text-foreground ring-offset-background hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:bg-muted/30 disabled:opacity-50';
8
-
9
- export type NativeSelectProps = React.ComponentPropsWithoutRef<'select'> & {
10
- wrapperClassName?: string;
11
- };
12
-
13
- export const NativeSelect = React.forwardRef<HTMLSelectElement, NativeSelectProps>(
14
- ({ className, wrapperClassName, children, ...props }, ref) => (
15
- <div className={cn('relative w-full', wrapperClassName)}>
16
- <select ref={ref} className={cn(nativeSelectClassName, className)} {...props}>
17
- {children}
18
- </select>
19
- <ChevronDown
20
- className="pointer-events-none absolute right-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground"
21
- aria-hidden
22
- />
23
- </div>
24
- )
25
- );
26
- NativeSelect.displayName = 'NativeSelect';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
OpenHands-Design/src/components/ui/search-input.tsx DELETED
@@ -1,74 +0,0 @@
1
- import * as React from 'react';
2
- import { Search, XCircle } from 'lucide-react';
3
- import { Input } from './input';
4
- import { cn } from '../../lib/utils';
5
-
6
- type InputProps = React.ComponentProps<typeof Input>;
7
- export type SearchInputProps = Omit<InputProps, 'type' | 'size'> & {
8
- value: string;
9
- onValueChange: (value: string) => void;
10
- /** Size: sm (h-9), default (h-10), lg (h-11) */
11
- size?: 'sm' | 'default' | 'lg';
12
- };
13
-
14
- const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
15
- (
16
- {
17
- value,
18
- onValueChange,
19
- placeholder,
20
- 'aria-label': ariaLabel,
21
- className,
22
- size = 'default',
23
- ...props
24
- },
25
- ref
26
- ) => {
27
- const sizeClasses = {
28
- sm: 'h-9 pl-9 pr-9',
29
- default: 'h-10 pl-10 pr-10',
30
- lg: 'h-11 pl-11 pr-11 text-base',
31
- };
32
- const iconSizes = {
33
- sm: 'h-4 w-4',
34
- default: 'h-4 w-4',
35
- lg: 'h-5 w-5',
36
- };
37
- const hasValue = value.length > 0;
38
-
39
- return (
40
- <div className={cn('relative w-full', className)}>
41
- <Search
42
- className={cn(
43
- 'absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none',
44
- iconSizes[size]
45
- )}
46
- aria-hidden
47
- />
48
- <Input
49
- ref={ref}
50
- type="search"
51
- value={value}
52
- onChange={(e) => onValueChange(e.target.value)}
53
- placeholder={placeholder}
54
- aria-label={ariaLabel}
55
- className={cn(sizeClasses[size])}
56
- {...props}
57
- />
58
- {hasValue && (
59
- <button
60
- type="button"
61
- onClick={() => onValueChange('')}
62
- className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full p-1 text-muted-foreground hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2"
63
- aria-label="Clear search"
64
- >
65
- <XCircle className={cn(iconSizes[size])} strokeWidth={2} />
66
- </button>
67
- )}
68
- </div>
69
- );
70
- }
71
- );
72
- SearchInput.displayName = 'SearchInput';
73
-
74
- export { SearchInput };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
OpenHands-Design/src/globals.css DELETED
@@ -1,135 +0,0 @@
1
- @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap');
2
- @tailwind base;
3
- @tailwind components;
4
- @tailwind utilities;
5
-
6
- @layer base {
7
- :root {
8
- --background: 0 0% 5%;
9
- --modal-background: var(--background);
10
- --foreground: 0 0% 98%;
11
-
12
- --card: 0 0% 7%;
13
- --card-foreground: 0 0% 98%;
14
-
15
- --popover: 0 0% 7%;
16
- --popover-foreground: 0 0% 98%;
17
-
18
- --primary: 0 0% 100%;
19
- --primary-foreground: 0 0% 0%;
20
-
21
- --secondary: 0 0% 8%;
22
- --secondary-foreground: 0 0% 98%;
23
-
24
- --muted: 0 0% 12%;
25
- --muted-foreground: 0 0% 55%;
26
- --muted-hover: 0 0% 18%;
27
-
28
- --accent: 0 0% 100%;
29
- --accent-foreground: 0 0% 0%;
30
-
31
- --destructive: 0 72% 51%;
32
- --destructive-foreground: 0 0% 98%;
33
-
34
- --border: 0 0% 14%;
35
- --input: 0 0% 14%;
36
- --ring: 0 0% 80%;
37
-
38
- --radius: 0.375rem;
39
- --radius-sm: 0.25rem;
40
- --radius-modal: 0.75rem;
41
-
42
- --success: 142 71% 45%;
43
- --success-foreground: 142 71% 76%;
44
- --warning: 38 92% 50%;
45
- --info: 217 91% 60%;
46
- --gradient-card-hover: linear-gradient(180deg, hsl(0 0% 9%) 0%, hsl(0 0% 7%) 100%);
47
-
48
- --shadow-card: 0 1px 2px 0 hsl(0 0% 0% / 0.3);
49
-
50
- --font-sans: 'Inter', system-ui, sans-serif;
51
- --font-mono: 'JetBrains Mono', monospace;
52
-
53
- --sidebar-background: 0 0% 5%;
54
- --sidebar-foreground: 0 0% 98%;
55
- --sidebar-primary: 0 0% 100%;
56
- --sidebar-primary-foreground: 0 0% 0%;
57
- --sidebar-accent: 0 0% 8%;
58
- --sidebar-accent-foreground: 0 0% 98%;
59
- --sidebar-border: 0 0% 14%;
60
- --sidebar-ring: 0 0% 50%;
61
- }
62
-
63
- .dark {
64
- --background: 0 0% 5%;
65
- --modal-background: var(--background);
66
- --foreground: 0 0% 98%;
67
-
68
- --card: 0 0% 7%;
69
- --card-foreground: 0 0% 98%;
70
-
71
- --popover: 0 0% 7%;
72
- --popover-foreground: 0 0% 98%;
73
-
74
- --primary: 0 0% 100%;
75
- --primary-foreground: 0 0% 0%;
76
-
77
- --secondary: 0 0% 8%;
78
- --secondary-foreground: 0 0% 98%;
79
-
80
- --muted: 0 0% 12%;
81
- --muted-foreground: 0 0% 55%;
82
- --muted-hover: 0 0% 18%;
83
-
84
- --accent: 0 0% 100%;
85
- --accent-foreground: 0 0% 0%;
86
-
87
- --destructive: 0 72% 51%;
88
- --destructive-foreground: 0 0% 98%;
89
-
90
- --border: 0 0% 14%;
91
- --input: 0 0% 14%;
92
- --ring: 0 0% 80%;
93
-
94
- --radius: 0.375rem;
95
- --radius-sm: 0.25rem;
96
- --radius-modal: 0.75rem;
97
-
98
- --success: 142 71% 45%;
99
- --success-foreground: 142 71% 76%;
100
- --warning: 38 92% 50%;
101
- --info: 217 91% 60%;
102
- --gradient-card-hover: linear-gradient(180deg, hsl(0 0% 9%) 0%, hsl(0 0% 7%) 100%);
103
-
104
- --shadow-card: 0 1px 2px 0 hsl(0 0% 0% / 0.3);
105
-
106
- --font-sans: 'Inter', system-ui, sans-serif;
107
- --font-mono: 'JetBrains Mono', monospace;
108
-
109
- --sidebar-background: 0 0% 5%;
110
- --sidebar-foreground: 0 0% 98%;
111
- --sidebar-primary: 0 0% 100%;
112
- --sidebar-primary-foreground: 0 0% 0%;
113
- --sidebar-accent: 0 0% 8%;
114
- --sidebar-accent-foreground: 0 0% 98%;
115
- --sidebar-border: 0 0% 14%;
116
- --sidebar-ring: 0 0% 50%;
117
- }
118
- }
119
-
120
- /* Strip native search chrome so focus ring matches all other inputs */
121
- input[type="search"] {
122
- -webkit-appearance: none;
123
- appearance: none;
124
- }
125
- input[type="search"]::-webkit-search-cancel-button {
126
- -webkit-appearance: none;
127
- appearance: none;
128
- display: none;
129
- }
130
- input[type="search"]::-webkit-search-decoration,
131
- input[type="search"]::-webkit-search-results-button,
132
- input[type="search"]::-webkit-search-results-decoration {
133
- -webkit-appearance: none;
134
- appearance: none;
135
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
OpenHands-Design/tailwind.config.js DELETED
@@ -1,99 +0,0 @@
1
- /** @type {import('tailwindcss').Config} */
2
- export default {
3
- darkMode: ["class"],
4
- content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
5
- theme: {
6
- container: {
7
- center: true,
8
- padding: "2rem",
9
- screens: {
10
- "2xl": "1400px",
11
- },
12
- },
13
- extend: {
14
- fontFamily: {
15
- sans: ["Inter", "system-ui", "sans-serif"],
16
- mono: ["JetBrains Mono", "monospace"],
17
- },
18
- colors: {
19
- border: "hsl(var(--border))",
20
- input: "hsl(var(--input))",
21
- ring: "hsl(var(--ring))",
22
- background: "hsl(var(--background))",
23
- modal: "hsl(var(--modal-background))",
24
- foreground: "hsl(var(--foreground))",
25
- primary: {
26
- DEFAULT: "hsl(var(--primary))",
27
- foreground: "hsl(var(--primary-foreground))",
28
- },
29
- secondary: {
30
- DEFAULT: "hsl(var(--secondary))",
31
- foreground: "hsl(var(--secondary-foreground))",
32
- },
33
- destructive: {
34
- DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
35
- foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
36
- },
37
- muted: {
38
- DEFAULT: "hsl(var(--muted))",
39
- foreground: "hsl(var(--muted-foreground))",
40
- hover: "hsl(var(--muted-hover))",
41
- },
42
- accent: {
43
- DEFAULT: "hsl(var(--accent))",
44
- foreground: "hsl(var(--accent-foreground))",
45
- },
46
- popover: {
47
- DEFAULT: "hsl(var(--popover))",
48
- foreground: "hsl(var(--popover-foreground))",
49
- },
50
- card: {
51
- DEFAULT: "hsl(var(--card))",
52
- foreground: "hsl(var(--card-foreground))",
53
- },
54
- sidebar: {
55
- DEFAULT: "hsl(var(--sidebar-background))",
56
- foreground: "hsl(var(--sidebar-foreground))",
57
- primary: "hsl(var(--sidebar-primary))",
58
- "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
59
- accent: "hsl(var(--sidebar-accent))",
60
- "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
61
- border: "hsl(var(--sidebar-border))",
62
- ring: "hsl(var(--sidebar-ring))",
63
- },
64
- success: {
65
- DEFAULT: "hsl(var(--success) / <alpha-value>)",
66
- foreground: "hsl(var(--success-foreground) / <alpha-value>)",
67
- },
68
- warning: "hsl(var(--warning) / <alpha-value>)",
69
- info: "hsl(var(--info) / <alpha-value>)",
70
- },
71
- borderRadius: {
72
- modal: "var(--radius-modal)",
73
- lg: "var(--radius)",
74
- md: "calc(var(--radius) - 2px)",
75
- sm: "calc(var(--radius) - 4px)",
76
- },
77
- keyframes: {
78
- "accordion-down": {
79
- from: { height: "0" },
80
- to: { height: "var(--radix-accordion-content-height)" },
81
- },
82
- "accordion-up": {
83
- from: { height: "var(--radix-accordion-content-height)" },
84
- to: { height: "0" },
85
- },
86
- "pulse-glow": {
87
- "0%, 100%": { opacity: "1" },
88
- "50%": { opacity: "0.5" },
89
- },
90
- },
91
- animation: {
92
- "accordion-down": "accordion-down 0.2s ease-out",
93
- "accordion-up": "accordion-up 0.2s ease-out",
94
- "pulse-glow": "pulse-glow 2s ease-in-out infinite",
95
- },
96
- },
97
- },
98
- plugins: [require("tailwindcss-animate")],
99
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
alternative_agents_page.py DELETED
@@ -1,103 +0,0 @@
1
- """Alternative Agents leaderboard page.
2
-
3
- The canonical OpenHands Index leaderboard (Home + the per-category pages)
4
- ranks default OpenHands agent runs from ``results/{model}/`` in the
5
- openhands-index-results repo. Third-party harnesses (Claude Code, Codex,
6
- Gemini CLI, OpenHands Sub-agents, ...) live under
7
- ``alternative_agents/{type}/{model}/`` and aren't directly comparable to
8
- default OpenHands runs (different scaffolds, different cost/runtime
9
- characteristics), so they get their own standalone page instead of being
10
- mixed into the same ranking.
11
-
12
- This page is intentionally a single Overall view (no per-category
13
- subpages) — the alternative-agents dataset is small (one row per
14
- harness × model) and the goal is "show me all the alternatives at a
15
- glance", not "drill into Issue Resolution for Codex".
16
-
17
- To make same-model comparisons easier, the page also appends canonical
18
- OpenHands rows for any language model that appears in the alternative
19
- agent dataset. The match is exact, so ``Gemini-3-Pro`` and
20
- ``Gemini-3.1-Pro`` remain distinct entries.
21
- """
22
- import matplotlib
23
- matplotlib.use('Agg')
24
- import pandas as pd
25
- import gradio as gr
26
-
27
- from simple_data_loader import SimpleLeaderboardViewer
28
- from ui_components import (
29
- create_leaderboard_display,
30
- get_full_leaderboard_data,
31
- )
32
-
33
-
34
- ALTERNATIVE_AGENTS_INTRO = """
35
- <div id="alternative-agents-intro">
36
- <h2>Alternative Agents</h2>
37
- <p>
38
- Third-party agent harnesses running the OpenHands Index benchmarks.
39
- To make direct comparisons easier, this page also includes the
40
- canonical OpenHands row whenever the exact same language model appears
41
- under an alternative harness. Cost and runtime numbers still come from
42
- each harness's own instrumentation and aren't directly comparable
43
- across harnesses.
44
- </p>
45
- </div>
46
- """
47
-
48
-
49
- def _append_openhands_shared_models(
50
- alternative_df: pd.DataFrame,
51
- split: str,
52
- ) -> pd.DataFrame:
53
- if alternative_df.empty or "Language Model" not in alternative_df.columns:
54
- return alternative_df
55
-
56
- openhands_df, _ = get_full_leaderboard_data(
57
- split,
58
- agent_filter=SimpleLeaderboardViewer.AGENT_FILTER_OPENHANDS,
59
- )
60
- if openhands_df.empty or "Language Model" not in openhands_df.columns:
61
- return alternative_df
62
-
63
- alternative_models = set(
64
- alternative_df["Language Model"].dropna().astype(str).str.strip()
65
- )
66
- if not alternative_models:
67
- return alternative_df
68
-
69
- openhands_shared_df = openhands_df[
70
- openhands_df["Language Model"].astype(str).str.strip().isin(alternative_models)
71
- ].copy()
72
- if openhands_shared_df.empty:
73
- return alternative_df
74
-
75
- return pd.concat([alternative_df, openhands_shared_df], ignore_index=True, sort=False)
76
-
77
-
78
- def build_page():
79
- gr.HTML(ALTERNATIVE_AGENTS_INTRO)
80
-
81
- gr.Markdown("---")
82
-
83
- test_df, test_tag_map = get_full_leaderboard_data(
84
- "test",
85
- agent_filter=SimpleLeaderboardViewer.AGENT_FILTER_ALTERNATIVE,
86
- )
87
-
88
- if test_df.empty:
89
- gr.Markdown(
90
- "No alternative agent submissions yet. New runs land in "
91
- "`alternative_agents/{type}/{model}/` in "
92
- "[openhands-index-results](https://github.com/OpenHands/openhands-index-results)."
93
- )
94
- return
95
-
96
- test_df = _append_openhands_shared_models(test_df, split="test")
97
-
98
- create_leaderboard_display(
99
- full_df=test_df,
100
- tag_map=test_tag_map,
101
- category_name="Overall",
102
- split_name="test",
103
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -35,7 +35,6 @@ from app_creation import build_page as build_app_creation_page
35
  from frontend_development import build_page as build_frontend_page
36
  from test_generation import build_page as build_test_generation_page
37
  from information_gathering import build_page as build_information_gathering_page
38
- from alternative_agents_page import build_page as build_alternative_agents_page
39
  from about import build_page as build_about_page
40
 
41
  logger.info(f"All modules imported (LOCAL_DEBUG={LOCAL_DEBUG})")
@@ -374,46 +373,20 @@ with demo.route("Testing", "/testing"):
374
  with demo.route("Information Gathering", "/information-gathering"):
375
  build_information_gathering_page()
376
 
377
- with demo.route("Alternative Agents", "/alternative-agents"):
378
- build_alternative_agents_page()
379
-
380
  with demo.route("About", "/about"):
381
  build_about_page()
382
 
383
  logger.info("All routes configured")
384
 
385
  # Mount the REST API on /api
386
- from fastapi import FastAPI, Request
387
- from fastapi.responses import RedirectResponse
388
- from starlette.middleware.base import BaseHTTPMiddleware
389
  from api import api_app
390
 
391
-
392
- class RootRedirectMiddleware(BaseHTTPMiddleware):
393
- """Middleware to redirect root path "/" to "/home".
394
-
395
- This fixes the 307 trailing slash redirect issue (Gradio bug #11071) that
396
- occurs when Gradio is mounted at "/" - FastAPI's default behavior redirects
397
- "/" to "//", which breaks routing on HuggingFace Spaces.
398
-
399
- See: https://github.com/gradio-app/gradio/issues/11071
400
- """
401
- async def dispatch(self, request: Request, call_next):
402
- if request.url.path == "/":
403
- return RedirectResponse(url="/home", status_code=302)
404
- return await call_next(request)
405
-
406
-
407
- # Create a parent FastAPI app with redirect_slashes=False to prevent
408
- # automatic trailing slash redirects that cause issues with Gradio
409
- root_app = FastAPI(redirect_slashes=False)
410
-
411
- # Add middleware to handle root path redirect to /home
412
- root_app.add_middleware(RootRedirectMiddleware)
413
-
414
  root_app.mount("/api", api_app)
415
 
416
- # Mount Gradio app at root path
417
  app = gr.mount_gradio_app(root_app, demo, path="/")
418
  logger.info("REST API mounted at /api, Gradio app mounted at /")
419
 
 
35
  from frontend_development import build_page as build_frontend_page
36
  from test_generation import build_page as build_test_generation_page
37
  from information_gathering import build_page as build_information_gathering_page
 
38
  from about import build_page as build_about_page
39
 
40
  logger.info(f"All modules imported (LOCAL_DEBUG={LOCAL_DEBUG})")
 
373
  with demo.route("Information Gathering", "/information-gathering"):
374
  build_information_gathering_page()
375
 
 
 
 
376
  with demo.route("About", "/about"):
377
  build_about_page()
378
 
379
  logger.info("All routes configured")
380
 
381
  # Mount the REST API on /api
382
+ from fastapi import FastAPI
 
 
383
  from api import api_app
384
 
385
+ # Create a parent FastAPI app that will host both the API and Gradio
386
+ root_app = FastAPI()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  root_app.mount("/api", api_app)
388
 
389
+ # Mount Gradio app - root redirect is handled by the proxy
390
  app = gr.mount_gradio_app(root_app, demo, path="/")
391
  logger.info("REST API mounted at /api, Gradio app mounted at /")
392
 
assets/harnesses/README.md DELETED
@@ -1,59 +0,0 @@
1
- # Agent harness logos
2
-
3
- This folder holds the **bottom half** of the composite scatter markers used
4
- on the [Alternative Agents](../../alternative_agents_page.py) page. Each
5
- point on that scatter stacks two logos: the model provider on top (from
6
- `assets/logo-*.svg`) and the harness on the bottom (from this folder).
7
-
8
- ## Expected filenames
9
-
10
- The scatter code looks up a logo by the exact `agent_name` string that the
11
- `push-to-index` workflow writes into the index repo's `metadata.json`, then
12
- maps it through `HARNESS_LOGO_STEMS` in `leaderboard_transformer.py`. Keep
13
- these filenames in sync with that map.
14
-
15
- | `agent_name` (in index repo) | File in this folder |
16
- | --- | --- |
17
- | `Claude Code` | `claude-code.svg` or `claude-code.png` |
18
- | `Codex` | `codex-cli.svg` or `codex-cli.png` |
19
- | `Gemini CLI` | `gemini-cli.svg` or `gemini-cli.png` |
20
- | `OpenHands` | `openhands.svg` or `openhands.png` |
21
- | `OpenHands Sub-agents` | `openhands.svg` or `openhands.png` (shared with `OpenHands`) |
22
-
23
- Both `.svg` and `.png` are accepted — the resolver tries `.svg` first, then
24
- `.png`. **Prefer SVG when possible**: the HuggingFace Space rejects new
25
- binary files on plain `git push` and routes PNGs through Xet, so an SVG is
26
- one less thing to set up.
27
-
28
- ## When a file is missing
29
-
30
- The scatter falls back to a single marker (just the model provider logo) —
31
- exactly the same rendering path the canonical OpenHands pages use. Nothing
32
- crashes and nothing prints a warning in normal operation. This means you
33
- can roll out logos one harness at a time without waiting for all four.
34
-
35
- ## Sizing and shape
36
-
37
- - Square canvas. The composite marker is drawn at a fixed aspect ratio, so
38
- a non-square logo will get squished.
39
- - Any SVG `viewBox` works — the renderer base64-encodes the file as-is and
40
- Plotly scales it to the marker's `sizex` / `sizey`. Around `80×80` to
41
- `256×256` is a good source size.
42
- - Leave some internal padding (≈10%) so the logo doesn't touch the marker
43
- edge when two are stacked.
44
- - No background is required, but a rounded-square coloured tile reads well
45
- at small sizes because it gives each harness a distinct silhouette even
46
- when the inner glyph isn't fully legible. Look at the existing
47
- `assets/logo-*.svg` files for the canonical model provider logos if you
48
- want a visual reference for sizing.
49
-
50
- ## Adding a new harness
51
-
52
- 1. Decide on the exact `agent_name` that the push-to-index workflow writes
53
- for the new harness (see `AGENT_NAME_BY_TYPE` in
54
- `OpenHands/evaluation/push-to-index-job/scripts/push_to_index_from_archive.py`).
55
- 2. Add an entry to `HARNESS_LOGO_STEMS` in
56
- [`leaderboard_transformer.py`](../../leaderboard_transformer.py) that
57
- maps the display name to a stem.
58
- 3. Drop `{stem}.svg` (or `.png`) into this folder.
59
- 4. Reload the app and look at `/alternative-agents`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/harnesses/claude-code.svg DELETED
assets/harnesses/codex-cli.svg DELETED
assets/harnesses/gemini-cli.svg DELETED
assets/harnesses/openhands.svg DELETED
assets/openhands-logotype-design.svg DELETED
assets/openhands-logotype-on-dark.svg DELETED
assets/openhands-logotype-on-light.svg DELETED
content.py CHANGED
@@ -556,11 +556,6 @@ span.wrap[tabindex="0"][role="button"][data-editable="false"] {
556
  grid-column: 8 !important;
557
  white-space: nowrap !important;
558
  }
559
- /* Hide the Alternative Agents page from the top-level nav for now. */
560
- .nav-holder nav a[href*="alternative-agents"] {
561
- display: none !important;
562
- }
563
-
564
  /* Divider line between header and category nav */
565
  .nav-holder nav::after {
566
  content: ''; /* Required for pseudo-elements to appear */
 
556
  grid-column: 8 !important;
557
  white-space: nowrap !important;
558
  }
 
 
 
 
 
559
  /* Divider line between header and category nav */
560
  .nav-holder nav::after {
561
  content: ''; /* Required for pseudo-elements to appear */
docs/screenshots/alternative-agents.png DELETED

Git LFS Details

  • SHA256: 99766c7d2c11a6f90f24a5f0effbae74a8aa33096b89ff1c4fcfb238fe06a2f5
  • Pointer size: 131 Bytes
  • Size of remote file: 104 kB
leaderboard_transformer.py CHANGED
@@ -228,17 +228,17 @@ def get_country_from_model(model_name: str) -> dict:
228
  def get_marker_icon(model_name: str, openness: str, mark_by: str) -> dict:
229
  """
230
  Gets the appropriate icon based on the mark_by selection.
231
-
232
  Args:
233
  model_name: The model name
234
  openness: The openness value (open/closed)
235
  mark_by: One of "Company", "Openness", or "Country"
236
-
237
  Returns:
238
  dict with 'path' and 'name' keys
239
  """
240
  from constants import MARK_BY_COMPANY, MARK_BY_OPENNESS, MARK_BY_COUNTRY
241
-
242
  if mark_by == MARK_BY_OPENNESS:
243
  return get_openness_icon(openness)
244
  elif mark_by == MARK_BY_COUNTRY:
@@ -247,59 +247,6 @@ def get_marker_icon(model_name: str, openness: str, mark_by: str) -> dict:
247
  return get_company_from_model(model_name)
248
 
249
 
250
- # Map the agent_name stored in the index repo's metadata.json to a file stem
251
- # inside assets/harnesses/. Kept in sync with AGENT_NAME_BY_TYPE in
252
- # OpenHands/evaluation push_to_index_from_archive.py — if a new ACP harness
253
- # lands there, add the corresponding display name and a matching stem here.
254
- #
255
- # The scatter plot looks for {stem}.svg first, then {stem}.png in
256
- # assets/harnesses/. This repo intentionally ships only a README in that
257
- # folder: drop the logo files in by hand (SVG preferred, PNG works too via
258
- # HF Xet) and they'll be picked up on the next app restart. If the file is
259
- # missing, get_harness_icon() returns None and the scatter falls back to the
260
- # single-marker path — same rendering the canonical OpenHands pages use —
261
- # so logos can be added one harness at a time without breaking anything.
262
- HARNESS_LOGO_STEMS: dict[str, str] = {
263
- "Claude Code": "claude-code",
264
- "Codex": "codex-cli",
265
- "Gemini CLI": "gemini-cli",
266
- "OpenHands": "openhands",
267
- "OpenHands Sub-agents": "openhands",
268
- }
269
- HARNESS_LOGO_DIR = "assets/harnesses"
270
- HARNESS_LOGO_EXTENSIONS = ("svg", "png")
271
-
272
-
273
- def get_harness_icon(agent_name: Optional[str]) -> Optional[dict]:
274
- """Return {'path', 'name'} for the harness logo, or None if not usable.
275
-
276
- Consumed by the Alternative Agents scatter plot to draw a composite
277
- marker (model provider on top, harness on bottom). Returns None in any
278
- of three cases, all of which make the caller skip the harness layer:
279
-
280
- - ``agent_name`` is empty or missing from the dataframe row.
281
- - ``agent_name`` isn't in ``HARNESS_LOGO_STEMS`` (new harness that
282
- hasn't been registered yet — register it and drop in a logo).
283
- - The logo file for that stem doesn't exist in ``assets/harnesses/``
284
- yet (the repo ships only the README).
285
-
286
- That third case is the important one: it lets the Alternative Agents
287
- page work immediately after checkout even when the harness logo files
288
- haven't been dropped in. The corresponding points just render like a
289
- canonical-page marker (model logo only) until the file is added.
290
- """
291
- if not agent_name:
292
- return None
293
- stem = HARNESS_LOGO_STEMS.get(str(agent_name).strip())
294
- if stem is None:
295
- return None
296
- for ext in HARNESS_LOGO_EXTENSIONS:
297
- path = f"{HARNESS_LOGO_DIR}/{stem}.{ext}"
298
- if os.path.exists(path):
299
- return {"path": path, "name": agent_name}
300
- return None
301
-
302
-
303
  # Standard layout configuration for all charts
304
  STANDARD_LAYOUT = dict(
305
  template="plotly_white",
@@ -708,7 +655,6 @@ def _pretty_column_name(raw_col: str) -> str:
708
  # Case 1: Handle fixed, special-case mappings first.
709
  fixed_mappings = {
710
  'id': 'id',
711
- 'agent_name': 'Agent',
712
  'SDK version': 'SDK Version',
713
  'Openhands version': 'SDK Version', # Legacy support
714
  'Language model': 'Language Model',
@@ -869,21 +815,7 @@ class DataTransformer:
869
  df_view = df_sorted.copy()
870
 
871
  # --- 3. Add Columns for Agent Openness ---
872
- # Only include the "Agent" column when the dataframe actually has
873
- # more than one distinct agent. On the canonical OpenHands pages
874
- # every row says "OpenHands", so adding the column is just noise;
875
- # on the Alternative Agents page rows differ (Claude Code / Codex
876
- # / Gemini CLI / OpenHands Sub-agents), so the column carries
877
- # signal and disambiguates same-model rows from different
878
- # harnesses.
879
- has_mixed_agents = (
880
- "Agent" in df_view.columns
881
- and df_view["Agent"].dropna().nunique() > 1
882
- )
883
- if has_mixed_agents:
884
- base_cols = ["id", "Agent", "Language Model", "SDK Version", "Source"]
885
- else:
886
- base_cols = ["id", "Language Model", "SDK Version", "Source"]
887
  new_cols = ["Openness"]
888
  ending_cols = ["Date", "Logs", "Visualization"]
889
 
@@ -970,8 +902,7 @@ def _plot_scatter_plotly(
970
  agent_col: str = 'Agent',
971
  name: Optional[str] = None,
972
  plot_type: str = 'cost', # 'cost' or 'runtime'
973
- mark_by: Optional[str] = None, # 'Company', 'Openness', or 'Country'
974
- show_all_labels: bool = False # Show labels for all points vs only Pareto frontier
975
  ) -> go.Figure:
976
  from constants import MARK_BY_DEFAULT
977
  if mark_by is None:
@@ -1087,18 +1018,13 @@ def _plot_scatter_plotly(
1087
  """
1088
  Builds the complete HTML string for the plot's hover tooltip.
1089
  Format: {lm_name} (SDK {version})
1090
- Harness: {agent} (only when the row carries an Agent —
1091
- Alternative Agents page only; the
1092
- canonical OpenHands pages drop the
1093
- Agent column in view() so this line
1094
- is skipped there)
1095
  Average Score: {score}
1096
  Average Cost/Runtime: {value}
1097
  Openness: {openness}
1098
  """
1099
  h_pad = " "
1100
  parts = ["<br>"]
1101
-
1102
  # Get and clean the language model name
1103
  llm_base_value = row.get('Language Model', '')
1104
  llm_base_value = clean_llm_base_list(llm_base_value)
@@ -1106,21 +1032,13 @@ def _plot_scatter_plotly(
1106
  lm_name = llm_base_value[0]
1107
  else:
1108
  lm_name = str(llm_base_value) if llm_base_value else 'Unknown'
1109
-
1110
  # Get SDK version
1111
  sdk_version = row.get('SDK Version', row.get(agent_col, 'Unknown'))
1112
-
1113
  # Title line: {lm_name} (SDK {version})
1114
  parts.append(f"{h_pad}<b>{lm_name}</b> (SDK {sdk_version}){h_pad}<br>")
1115
-
1116
- # Harness line — only on pages where the Agent column is present
1117
- # (Alternative Agents). Without this, two rows for the same LM run
1118
- # under different harnesses (e.g. Claude Code vs OpenHands Sub-agents
1119
- # on claude-sonnet-4-5) are indistinguishable on hover.
1120
- agent_value = row.get('Agent')
1121
- if agent_value is not None and pd.notna(agent_value) and str(agent_value).strip():
1122
- parts.append(f"{h_pad}Harness: <b>{agent_value}</b>{h_pad}<br>")
1123
-
1124
  # Average Score
1125
  parts.append(f"{h_pad}Average Score: <b>{row[y_col]:.3f}</b>{h_pad}<br>")
1126
 
@@ -1193,182 +1111,103 @@ def _plot_scatter_plotly(
1193
  y_min = min_score - 5 if min_score > 5 else 0
1194
  y_max = max_score + 5
1195
 
1196
- # Cache base64-encoded logos across rows — every Claude model on the
1197
- # Alternative Agents page points at the same assets/harness-claude-code.svg,
1198
- # so decoding once per path is ~N× cheaper than once per point.
1199
- _logo_cache: dict[str, str] = {}
1200
- def _encode_logo(path: str) -> Optional[str]:
1201
- if path in _logo_cache:
1202
- return _logo_cache[path]
1203
- if not os.path.exists(path):
1204
- return None
1205
- try:
1206
- with open(path, "rb") as f:
1207
- encoded = base64.b64encode(f.read()).decode("utf-8")
1208
- except Exception as e:
1209
- logger.warning(f"Could not load logo {path}: {e}")
1210
- return None
1211
- mime = "svg+xml" if path.lower().endswith(".svg") else "png"
1212
- uri = f"data:image/{mime};base64,{encoded}"
1213
- _logo_cache[path] = uri
1214
- return uri
1215
-
1216
- # Composite markers: on the Alternative Agents page the dataframe carries
1217
- # an "Agent" column (Claude Code / Codex / Gemini CLI / OpenHands Sub-agents),
1218
- # so a point for claude-sonnet-4-5 under Claude Code and under OpenHands
1219
- # Sub-agents would otherwise share the exact same Anthropic logo marker
1220
- # and be visually indistinguishable. When Agent is present, we stack
1221
- # two logos at each point: model provider on top, harness on the bottom.
1222
- # Canonical OpenHands pages drop the Agent column in view() (via the
1223
- # has_mixed_agents check), so they fall through to the single-logo path
1224
- # and render exactly as before.
1225
- has_harness_column = (
1226
- "Agent" in data_plot.columns
1227
- and data_plot["Agent"].dropna().astype(str).str.strip().ne("").any()
1228
- )
1229
-
1230
- # Marker sizes. The composite variant fits two logos inside roughly the
1231
- # same vertical footprint as a single marker, so each half is slightly
1232
- # smaller and the two halves are offset symmetrically around the point's
1233
- # true y-coordinate.
1234
- SINGLE_SIZE_X, SINGLE_SIZE_Y = 0.04, 0.06
1235
- STACKED_SIZE_X, STACKED_SIZE_Y = 0.035, 0.048
1236
- STACKED_Y_OFFSET = 0.028 # half-separation between model (top) and harness (bottom)
1237
-
1238
  for _, row in data_plot.iterrows():
1239
  model_name = row.get('Language Model', '')
1240
  openness = row.get('Openness', '')
1241
  marker_info = get_marker_icon(model_name, openness, mark_by)
1242
- model_logo_uri = _encode_logo(marker_info['path'])
1243
- if model_logo_uri is None:
1244
- continue
1245
-
1246
- # Harness (only meaningful when the dataframe carries an Agent column).
1247
- harness_uri = None
1248
- if has_harness_column:
1249
- harness_info = get_harness_icon(row.get("Agent"))
1250
- if harness_info is not None:
1251
- harness_uri = _encode_logo(harness_info["path"])
1252
-
1253
- x_val = row[x_col_to_use]
1254
- y_val = row[y_col_to_use]
1255
-
1256
- # Convert to domain coordinates (0-1 range)
1257
- # For log scale x: domain_x = (log10(x) - x_min_log) / (x_max_log - x_min_log)
1258
- if x_val > 0:
1259
- log_x = np.log10(x_val)
1260
- domain_x = (log_x - x_min_log) / (x_max_log - x_min_log)
1261
- else:
1262
- domain_x = 0
1263
-
1264
- # For linear y: domain_y = (y - y_min) / (y_max - y_min)
1265
- domain_y = (y_val - y_min) / (y_max - y_min) if (y_max - y_min) > 0 else 0.5
1266
-
1267
- # Clamp to valid range
1268
- domain_x = max(0, min(1, domain_x))
1269
- domain_y = max(0, min(1, domain_y))
1270
-
1271
- # Convert to data coordinates
1272
- # For log scale x: use log10(x) to match the axis type
1273
- x_log = np.log10(x_val) if x_val > 0 else x_min_log
1274
-
1275
- if harness_uri is not None:
1276
- # Composite: stack model on top, harness on bottom
1277
- # Use data coordinates (x, y) so logos zoom/pan together with labels
1278
- y_offset = 0.8 # Offset above the data point (in score units)
1279
- layout_images.append(dict(
1280
- source=model_logo_uri,
1281
- xref="x", yref="y",
1282
- x=x_log, y=y_val + y_offset,
1283
- sizex=STACKED_SIZE_X * (x_max_log - x_min_log),
1284
- sizey=STACKED_SIZE_Y * (y_max - y_min),
1285
- xanchor="center", yanchor="middle",
1286
- layer="above",
1287
- ))
1288
- layout_images.append(dict(
1289
- source=harness_uri,
1290
- xref="x", yref="y",
1291
- x=x_log, y=y_val - y_offset,
1292
- sizex=STACKED_SIZE_X * (x_max_log - x_min_log),
1293
- sizey=STACKED_SIZE_Y * (y_max - y_min),
1294
- xanchor="center", yanchor="middle",
1295
- layer="above",
1296
- ))
1297
- else:
1298
- # Single marker - use data coordinates so logo zooms/pans with labels
1299
- layout_images.append(dict(
1300
- source=model_logo_uri,
1301
- xref="x", yref="y",
1302
- x=x_log, y=y_val,
1303
- sizex=SINGLE_SIZE_X * (x_max_log - x_min_log),
1304
- sizey=SINGLE_SIZE_Y * (y_max - y_min),
1305
- xanchor="center", yanchor="middle",
1306
- layer="above",
1307
- ))
1308
 
1309
- # --- Section 7: Add Model Name Labels ---
1310
- # Show labels for all points if show_all_labels is True, otherwise just Pareto frontier
1311
- if show_all_labels:
1312
- # Label all data points
1313
- labels_data = []
1314
- for _, row in data_plot.iterrows():
1315
- x_val = row[x_col_to_use]
1316
- y_val = row[y_col_to_use]
1317
-
1318
- model_name = row.get('Language Model', '')
1319
- if isinstance(model_name, list):
1320
- model_name = model_name[0] if model_name else ''
1321
- model_name = str(model_name).split('/')[-1]
1322
- if len(model_name) > 25:
1323
- model_name = model_name[:22] + '...'
1324
-
1325
- labels_data.append({'x': x_val, 'y': y_val, 'label': model_name})
1326
- elif frontier_rows:
1327
- # Label only Pareto frontier points
1328
- labels_data = []
1329
 
1330
  for row in frontier_rows:
1331
  x_val = row[x_col_to_use]
1332
  y_val = row[y_col_to_use]
1333
 
 
1334
  model_name = row.get('Language Model', '')
1335
  if isinstance(model_name, list):
1336
  model_name = model_name[0] if model_name else ''
 
1337
  model_name = str(model_name).split('/')[-1]
 
1338
  if len(model_name) > 25:
1339
  model_name = model_name[:22] + '...'
1340
 
1341
- labels_data.append({'x': x_val, 'y': y_val, 'label': model_name})
1342
- else:
1343
- labels_data = []
1344
-
1345
- # Add annotations for each label
1346
- # For log scale x-axis, annotations need log10(x) coordinates (Plotly issue #2580)
1347
- for item in labels_data:
1348
- x_val = item['x']
1349
- y_val = item['y']
1350
- label = item['label']
1351
 
1352
- # Transform x to log10 for annotation positioning on log scale
1353
- if x_val > 0:
1354
- x_log = np.log10(x_val)
1355
- else:
1356
- x_log = x_min_log
1357
-
1358
- fig.add_annotation(
1359
- x=x_log,
1360
- y=y_val,
1361
- text=label,
1362
- showarrow=False,
1363
- yshift=25, # Move label higher above the icon
1364
- font=dict(
1365
- size=10,
1366
- color='#0D0D0F', # neutral-950
1367
- family=FONT_FAMILY_SHORT
1368
- ),
1369
- xanchor='center',
1370
- yanchor='bottom'
1371
- )
 
 
 
 
 
 
 
1372
 
1373
  # --- Section 8: Configure Layout ---
1374
  # Use the same axis ranges as calculated for domain coordinates
 
228
  def get_marker_icon(model_name: str, openness: str, mark_by: str) -> dict:
229
  """
230
  Gets the appropriate icon based on the mark_by selection.
231
+
232
  Args:
233
  model_name: The model name
234
  openness: The openness value (open/closed)
235
  mark_by: One of "Company", "Openness", or "Country"
236
+
237
  Returns:
238
  dict with 'path' and 'name' keys
239
  """
240
  from constants import MARK_BY_COMPANY, MARK_BY_OPENNESS, MARK_BY_COUNTRY
241
+
242
  if mark_by == MARK_BY_OPENNESS:
243
  return get_openness_icon(openness)
244
  elif mark_by == MARK_BY_COUNTRY:
 
247
  return get_company_from_model(model_name)
248
 
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  # Standard layout configuration for all charts
251
  STANDARD_LAYOUT = dict(
252
  template="plotly_white",
 
655
  # Case 1: Handle fixed, special-case mappings first.
656
  fixed_mappings = {
657
  'id': 'id',
 
658
  'SDK version': 'SDK Version',
659
  'Openhands version': 'SDK Version', # Legacy support
660
  'Language model': 'Language Model',
 
815
  df_view = df_sorted.copy()
816
 
817
  # --- 3. Add Columns for Agent Openness ---
818
+ base_cols = ["id","Language Model","SDK Version","Source"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
819
  new_cols = ["Openness"]
820
  ending_cols = ["Date", "Logs", "Visualization"]
821
 
 
902
  agent_col: str = 'Agent',
903
  name: Optional[str] = None,
904
  plot_type: str = 'cost', # 'cost' or 'runtime'
905
+ mark_by: Optional[str] = None # 'Company', 'Openness', or 'Country'
 
906
  ) -> go.Figure:
907
  from constants import MARK_BY_DEFAULT
908
  if mark_by is None:
 
1018
  """
1019
  Builds the complete HTML string for the plot's hover tooltip.
1020
  Format: {lm_name} (SDK {version})
 
 
 
 
 
1021
  Average Score: {score}
1022
  Average Cost/Runtime: {value}
1023
  Openness: {openness}
1024
  """
1025
  h_pad = " "
1026
  parts = ["<br>"]
1027
+
1028
  # Get and clean the language model name
1029
  llm_base_value = row.get('Language Model', '')
1030
  llm_base_value = clean_llm_base_list(llm_base_value)
 
1032
  lm_name = llm_base_value[0]
1033
  else:
1034
  lm_name = str(llm_base_value) if llm_base_value else 'Unknown'
1035
+
1036
  # Get SDK version
1037
  sdk_version = row.get('SDK Version', row.get(agent_col, 'Unknown'))
1038
+
1039
  # Title line: {lm_name} (SDK {version})
1040
  parts.append(f"{h_pad}<b>{lm_name}</b> (SDK {sdk_version}){h_pad}<br>")
1041
+
 
 
 
 
 
 
 
 
1042
  # Average Score
1043
  parts.append(f"{h_pad}Average Score: <b>{row[y_col]:.3f}</b>{h_pad}<br>")
1044
 
 
1111
  y_min = min_score - 5 if min_score > 5 else 0
1112
  y_max = max_score + 5
1113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1114
  for _, row in data_plot.iterrows():
1115
  model_name = row.get('Language Model', '')
1116
  openness = row.get('Openness', '')
1117
  marker_info = get_marker_icon(model_name, openness, mark_by)
1118
+ logo_path = marker_info['path']
1119
+
1120
+ # Read the SVG file and encode as base64 data URI
1121
+ if os.path.exists(logo_path):
1122
+ try:
1123
+ with open(logo_path, 'rb') as f:
1124
+ encoded_logo = base64.b64encode(f.read()).decode('utf-8')
1125
+ logo_uri = f"data:image/svg+xml;base64,{encoded_logo}"
1126
+
1127
+ x_val = row[x_col_to_use]
1128
+ y_val = row[y_col_to_use]
1129
+
1130
+ # Convert to domain coordinates (0-1 range)
1131
+ # For log scale x: domain_x = (log10(x) - x_min_log) / (x_max_log - x_min_log)
1132
+ if x_val > 0:
1133
+ log_x = np.log10(x_val)
1134
+ domain_x = (log_x - x_min_log) / (x_max_log - x_min_log)
1135
+ else:
1136
+ domain_x = 0
1137
+
1138
+ # For linear y: domain_y = (y - y_min) / (y_max - y_min)
1139
+ domain_y = (y_val - y_min) / (y_max - y_min) if (y_max - y_min) > 0 else 0.5
1140
+
1141
+ # Clamp to valid range
1142
+ domain_x = max(0, min(1, domain_x))
1143
+ domain_y = max(0, min(1, domain_y))
1144
+
1145
+ layout_images.append(dict(
1146
+ source=logo_uri,
1147
+ xref="x domain", # Use domain coordinates for log scale compatibility
1148
+ yref="y domain",
1149
+ x=domain_x,
1150
+ y=domain_y,
1151
+ sizex=0.04, # Size as fraction of plot width
1152
+ sizey=0.06, # Size as fraction of plot height
1153
+ xanchor="center",
1154
+ yanchor="middle",
1155
+ layer="above"
1156
+ ))
1157
+ except Exception as e:
1158
+ logger.warning(f"Could not load logo {logo_path}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1159
 
1160
+ # --- Section 7: Add Model Name Labels to Frontier Points ---
1161
+ if frontier_rows:
1162
+ frontier_labels_data = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1163
 
1164
  for row in frontier_rows:
1165
  x_val = row[x_col_to_use]
1166
  y_val = row[y_col_to_use]
1167
 
1168
+ # Get the model name for the label
1169
  model_name = row.get('Language Model', '')
1170
  if isinstance(model_name, list):
1171
  model_name = model_name[0] if model_name else ''
1172
+ # Clean the model name (remove path prefixes)
1173
  model_name = str(model_name).split('/')[-1]
1174
+ # Truncate long names
1175
  if len(model_name) > 25:
1176
  model_name = model_name[:22] + '...'
1177
 
1178
+ frontier_labels_data.append({
1179
+ 'x': x_val,
1180
+ 'y': y_val,
1181
+ 'label': model_name
1182
+ })
 
 
 
 
 
1183
 
1184
+ # Add annotations for each frontier label
1185
+ # For log scale x-axis, annotations need log10(x) coordinates (Plotly issue #2580)
1186
+ for item in frontier_labels_data:
1187
+ x_val = item['x']
1188
+ y_val = item['y']
1189
+ label = item['label']
1190
+
1191
+ # Transform x to log10 for annotation positioning on log scale
1192
+ if x_val > 0:
1193
+ x_log = np.log10(x_val)
1194
+ else:
1195
+ x_log = x_min_log
1196
+
1197
+ fig.add_annotation(
1198
+ x=x_log,
1199
+ y=y_val,
1200
+ text=label,
1201
+ showarrow=False,
1202
+ yshift=25, # Move label higher above the icon
1203
+ font=dict(
1204
+ size=10,
1205
+ color='#0D0D0F', # neutral-950
1206
+ family=FONT_FAMILY_SHORT
1207
+ ),
1208
+ xanchor='center',
1209
+ yanchor='bottom'
1210
+ )
1211
 
1212
  # --- Section 8: Configure Layout ---
1213
  # Use the same axis ranges as calculated for domain coordinates
main_page.py CHANGED
@@ -1,7 +1,6 @@
1
  import matplotlib
2
  matplotlib.use('Agg')
3
  import gradio as gr
4
- import pandas as pd
5
 
6
 
7
  from ui_components import (
@@ -27,32 +26,6 @@ from constants import MARK_BY_DEFAULT
27
  CACHED_VIEWERS = {}
28
  CACHED_TAG_MAPS = {}
29
 
30
-
31
- def filter_complete_entries(df: pd.DataFrame) -> pd.DataFrame:
32
- if df.empty:
33
- return df.copy()
34
-
35
- category_score_columns = [
36
- 'Issue Resolution Score',
37
- 'Frontend Score',
38
- 'Greenfield Score',
39
- 'Testing Score',
40
- 'Information Gathering Score',
41
- ]
42
-
43
- if all(column in df.columns for column in category_score_columns):
44
- return df[df[category_score_columns].notna().all(axis=1)].copy()
45
-
46
- if 'Categories Completed' in df.columns:
47
- categories_completed = pd.to_numeric(df['Categories Completed'], errors='coerce')
48
- return df[categories_completed >= 5].copy()
49
-
50
- if 'Categories Attempted' in df.columns:
51
- return df[df['Categories Attempted'] == '5/5'].copy()
52
-
53
- return df.copy()
54
-
55
-
56
  def build_page():
57
  with gr.Row(elem_id="intro-row"):
58
  with gr.Column(scale=1):
@@ -65,91 +38,78 @@ def build_page():
65
 
66
  test_df, test_tag_map = get_full_leaderboard_data("test")
67
  if not test_df.empty:
68
- show_incomplete_checkbox, show_open_only_checkbox, mark_by_dropdown = create_leaderboard_display(
 
69
  full_df=test_df,
70
  tag_map=test_tag_map,
71
  category_name=CATEGORY_NAME,
72
  split_name="test"
73
  )
74
-
75
- test_df_complete = filter_complete_entries(test_df)
76
- has_complete_entries = len(test_df_complete) > 0
77
-
78
  if 'Openness' in test_df.columns:
79
  test_df_open = test_df[test_df['Openness'].str.lower() == 'open'].copy()
80
  else:
81
  test_df_open = test_df.copy()
82
- test_df_complete_open = filter_complete_entries(test_df_open)
83
-
84
- initial_df = test_df_complete if has_complete_entries else test_df
85
-
86
  # --- Winners by Category Section ---
87
  gr.Markdown("---")
88
  gr.HTML('<h2>Winners by Category</h2>', elem_id="winners-header")
89
  gr.Markdown("Top 5 performing systems in each benchmark category.")
90
-
91
- winners_component = gr.HTML(
92
- create_winners_by_category_html(initial_df, top_n=5),
93
- elem_id="winners-by-category",
94
- )
95
-
 
96
  # --- New Visualization Sections ---
97
  gr.Markdown("---")
98
-
99
  # Evolution Over Time Section
100
  gr.HTML('<h2>Evolution Over Time</h2>', elem_id="evolution-header")
101
  gr.Markdown("Track how model performance has improved over time based on release dates.")
102
-
103
- evolution_component = gr.Plot(
104
- value=create_evolution_over_time_chart(initial_df, MARK_BY_DEFAULT),
105
- elem_id="evolution-chart",
106
- )
107
-
108
  gr.Markdown("---")
109
-
110
  # Open Model Accuracy by Size Section (always shows open models only by design)
111
  gr.HTML('<h2>Open Model Accuracy by Size</h2>', elem_id="size-accuracy-header")
112
  gr.Markdown("Compare open-weights model performance against their parameter count.")
113
-
114
- size_component = gr.Plot(
115
- value=create_accuracy_by_size_chart(initial_df, MARK_BY_DEFAULT),
116
- elem_id="size-accuracy-chart",
117
- )
118
-
119
- def update_extra_sections(show_incomplete, show_open_only, mark_by):
120
- include_incomplete = show_incomplete or not has_complete_entries
121
- base_df = test_df if include_incomplete else test_df_complete
122
- base_df_open = test_df_open if include_incomplete else test_df_complete_open
123
- winners_df = base_df_open if show_open_only else base_df
124
-
125
- winners_html = create_winners_by_category_html(winners_df, top_n=5)
126
- evolution_fig = create_evolution_over_time_chart(winners_df, mark_by)
127
- size_fig = create_accuracy_by_size_chart(base_df, mark_by)
128
-
129
  return winners_html, evolution_fig, size_fig
130
-
131
- show_incomplete_input = show_incomplete_checkbox if show_incomplete_checkbox is not None else gr.State(value=True)
132
- show_open_only_input = show_open_only_checkbox if show_open_only_checkbox is not None else gr.State(value=False)
133
- extra_section_inputs = [show_incomplete_input, show_open_only_input, mark_by_dropdown]
134
-
135
- if show_incomplete_checkbox is not None:
136
- show_incomplete_checkbox.change(
137
- fn=update_extra_sections,
138
- inputs=extra_section_inputs,
139
- outputs=[winners_component, evolution_component, size_component]
140
- )
141
-
142
  if show_open_only_checkbox is not None:
143
  show_open_only_checkbox.change(
144
  fn=update_extra_sections,
145
- inputs=extra_section_inputs,
146
  outputs=[winners_component, evolution_component, size_component]
147
  )
148
-
149
  if mark_by_dropdown is not None:
150
  mark_by_dropdown.change(
151
  fn=update_extra_sections,
152
- inputs=extra_section_inputs,
153
  outputs=[winners_component, evolution_component, size_component]
154
  )
155
 
 
1
  import matplotlib
2
  matplotlib.use('Agg')
3
  import gradio as gr
 
4
 
5
 
6
  from ui_components import (
 
26
  CACHED_VIEWERS = {}
27
  CACHED_TAG_MAPS = {}
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  def build_page():
30
  with gr.Row(elem_id="intro-row"):
31
  with gr.Column(scale=1):
 
38
 
39
  test_df, test_tag_map = get_full_leaderboard_data("test")
40
  if not test_df.empty:
41
+ # Get the checkbox and dropdown returned from create_leaderboard_display
42
+ show_open_only_checkbox, mark_by_dropdown = create_leaderboard_display(
43
  full_df=test_df,
44
  tag_map=test_tag_map,
45
  category_name=CATEGORY_NAME,
46
  split_name="test"
47
  )
48
+
49
+ # Prepare open-only filtered dataframe for Winners and Evolution
 
 
50
  if 'Openness' in test_df.columns:
51
  test_df_open = test_df[test_df['Openness'].str.lower() == 'open'].copy()
52
  else:
53
  test_df_open = test_df.copy()
54
+
 
 
 
55
  # --- Winners by Category Section ---
56
  gr.Markdown("---")
57
  gr.HTML('<h2>Winners by Category</h2>', elem_id="winners-header")
58
  gr.Markdown("Top 5 performing systems in each benchmark category.")
59
+
60
+ # Create both all and open-only versions of winners HTML
61
+ winners_html_all = create_winners_by_category_html(test_df, top_n=5)
62
+ winners_html_open = create_winners_by_category_html(test_df_open, top_n=5)
63
+
64
+ winners_component = gr.HTML(winners_html_all, elem_id="winners-by-category")
65
+
66
  # --- New Visualization Sections ---
67
  gr.Markdown("---")
68
+
69
  # Evolution Over Time Section
70
  gr.HTML('<h2>Evolution Over Time</h2>', elem_id="evolution-header")
71
  gr.Markdown("Track how model performance has improved over time based on release dates.")
72
+
73
+ # Create initial evolution chart with default mark_by
74
+ evolution_fig_all = create_evolution_over_time_chart(test_df, MARK_BY_DEFAULT)
75
+
76
+ evolution_component = gr.Plot(value=evolution_fig_all, elem_id="evolution-chart")
77
+
78
  gr.Markdown("---")
79
+
80
  # Open Model Accuracy by Size Section (always shows open models only by design)
81
  gr.HTML('<h2>Open Model Accuracy by Size</h2>', elem_id="size-accuracy-header")
82
  gr.Markdown("Compare open-weights model performance against their parameter count.")
83
+
84
+ size_fig = create_accuracy_by_size_chart(test_df, MARK_BY_DEFAULT)
85
+ size_component = gr.Plot(value=size_fig, elem_id="size-accuracy-chart")
86
+
87
+ # Update function for Winners, Evolution, and Size charts based on filters
88
+ def update_extra_sections(show_open_only, mark_by):
89
+ # Select the appropriate dataframe based on open_only filter
90
+ df_to_use = test_df_open if show_open_only else test_df
91
+
92
+ # Winners HTML (not affected by mark_by, only open_only)
93
+ winners_html = winners_html_open if show_open_only else winners_html_all
94
+
95
+ # Regenerate charts with current mark_by setting
96
+ evolution_fig = create_evolution_over_time_chart(df_to_use, mark_by)
97
+ size_fig = create_accuracy_by_size_chart(test_df, mark_by) # Size chart always uses full df (filters internally)
98
+
99
  return winners_html, evolution_fig, size_fig
100
+
101
+ # Connect both checkbox and dropdown to update all extra sections
 
 
 
 
 
 
 
 
 
 
102
  if show_open_only_checkbox is not None:
103
  show_open_only_checkbox.change(
104
  fn=update_extra_sections,
105
+ inputs=[show_open_only_checkbox, mark_by_dropdown],
106
  outputs=[winners_component, evolution_component, size_component]
107
  )
108
+
109
  if mark_by_dropdown is not None:
110
  mark_by_dropdown.change(
111
  fn=update_extra_sections,
112
+ inputs=[show_open_only_checkbox if show_open_only_checkbox else gr.State(value=False), mark_by_dropdown],
113
  outputs=[winners_component, evolution_component, size_component]
114
  )
115
 
setup_data.py CHANGED
@@ -70,39 +70,27 @@ def fetch_data_from_github():
70
 
71
  # Look for data files in the cloned repository
72
  results_source = clone_dir / "results"
73
-
74
  if not results_source.exists():
75
  print(f"Results directory not found in repository")
76
  return False
77
-
78
  # Check if there are any agent result directories
79
  result_dirs = list(results_source.iterdir())
80
  if not result_dirs:
81
  print(f"No agent results found in {results_source}")
82
  return False
83
-
84
  print(f"Found {len(result_dirs)} agent result directories")
85
-
86
  # Create target directory and copy the results structure
87
  os.makedirs(target_dir.parent, exist_ok=True)
88
  if target_dir.exists():
89
  shutil.rmtree(target_dir)
90
-
91
  # Copy the entire results directory
92
  target_results = target_dir / "results"
93
  shutil.copytree(results_source, target_results)
94
-
95
- # Also copy alternative_agents/ if present, so the loader can pick up
96
- # ACP runs (acp-claude, acp-codex, acp-gemini, openhands_subagents, ...)
97
- # alongside the default OpenHands agent results.
98
- alt_source = clone_dir / "alternative_agents"
99
- if alt_source.exists():
100
- alt_target = target_dir / "alternative_agents"
101
- shutil.copytree(alt_source, alt_target)
102
- agent_types = sorted(p.name for p in alt_source.iterdir() if p.is_dir())
103
- print(f"Found alternative agent types: {agent_types}")
104
- else:
105
- print("No alternative_agents/ directory in repository (skipping)")
106
 
107
  print(f"Successfully fetched data from GitHub. Files: {list(target_dir.glob('*'))}")
108
 
 
70
 
71
  # Look for data files in the cloned repository
72
  results_source = clone_dir / "results"
73
+
74
  if not results_source.exists():
75
  print(f"Results directory not found in repository")
76
  return False
77
+
78
  # Check if there are any agent result directories
79
  result_dirs = list(results_source.iterdir())
80
  if not result_dirs:
81
  print(f"No agent results found in {results_source}")
82
  return False
83
+
84
  print(f"Found {len(result_dirs)} agent result directories")
85
+
86
  # Create target directory and copy the results structure
87
  os.makedirs(target_dir.parent, exist_ok=True)
88
  if target_dir.exists():
89
  shutil.rmtree(target_dir)
90
+
91
  # Copy the entire results directory
92
  target_results = target_dir / "results"
93
  shutil.copytree(results_source, target_results)
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  print(f"Successfully fetched data from GitHub. Files: {list(target_dir.glob('*'))}")
96
 
simple_data_loader.py CHANGED
@@ -96,43 +96,17 @@ def load_and_validate_agent_data(agent_dir: Path) -> tuple[Optional[dict], Optio
96
 
97
  class SimpleLeaderboardViewer:
98
  """Simple replacement for agent-eval's LeaderboardViewer."""
99
-
100
- AGENT_FILTER_OPENHANDS = "openhands"
101
- AGENT_FILTER_ALTERNATIVE = "alternative"
102
-
103
- def __init__(
104
- self,
105
- data_dir: str,
106
- config: str,
107
- split: str,
108
- agent_filter: str = AGENT_FILTER_OPENHANDS,
109
- ):
110
  """
111
  Args:
112
  data_dir: Path to data directory
113
  config: Config name (e.g., "1.0.0-dev1")
114
  split: Split name (e.g., "validation" or "test")
115
- agent_filter: Which submissions to include.
116
- ``"openhands"`` (default) loads only the default OpenHands
117
- agent runs from ``results/{model}/`` — the canonical
118
- leaderboard. ``"alternative"`` loads only third-party
119
- harnesses (Claude Code / Codex / Gemini CLI / OpenHands
120
- Sub-agents) from ``alternative_agents/{type}/{model}/``,
121
- which power the standalone Alternative Agents page.
122
- The two are kept on separate pages because their
123
- cost/runtime numbers aren't apples-to-apples and mixing
124
- them in one ranking would be misleading.
125
  """
126
- if agent_filter not in (self.AGENT_FILTER_OPENHANDS, self.AGENT_FILTER_ALTERNATIVE):
127
- raise ValueError(
128
- f"agent_filter must be one of "
129
- f"{{{self.AGENT_FILTER_OPENHANDS!r}, {self.AGENT_FILTER_ALTERNATIVE!r}}}, "
130
- f"got {agent_filter!r}"
131
- )
132
  self.data_dir = Path(data_dir)
133
  self.config = config
134
  self.split = split
135
- self.agent_filter = agent_filter
136
  self.config_path = self.data_dir / config
137
 
138
  # Benchmark to category mappings (single source of truth)
@@ -153,116 +127,55 @@ class SimpleLeaderboardViewer:
153
  if benchmark not in self.tag_map[category]:
154
  self.tag_map[category].append(benchmark)
155
 
156
- # Default agent_name when metadata.json doesn't carry one. Matches the
157
- # default-agent value used by push_to_index_from_archive.py so legacy
158
- # entries (which omit the field) still group cleanly with new entries.
159
- DEFAULT_AGENT_NAME = "OpenHands"
160
-
161
- def _records_from_agent_dir(self, agent_dir: Path, default_agent_name: str | None = None) -> tuple[list[dict], list[str]]:
162
- """Build per-benchmark records from a single agent directory.
163
-
164
- Shared by ``_load_from_agent_dirs`` (default OpenHands results) and
165
- ``_load_from_alternative_agents_dirs`` (acp-claude / acp-codex / etc.).
166
- Returns ``(records, validation_errors)``. Returns an empty list of
167
- records when the directory has no scores or is hidden from the
168
- leaderboard.
169
- """
170
- records: list[dict] = []
171
- metadata, scores, errors = load_and_validate_agent_data(agent_dir)
172
-
173
- if metadata is None or scores is None:
174
- return records, errors
175
-
176
- if metadata.get('hide_from_leaderboard', False):
177
- logger.info(f"Skipping {agent_dir.name}: hide_from_leaderboard is True")
178
- return records, errors
179
-
180
- # Resolve the agent display name. Prefer the value stamped into
181
- # metadata.json by push-to-index; fall back to the directory's
182
- # default (e.g. "Claude Code" for acp-claude/) and finally to
183
- # "OpenHands" for legacy results/ entries that predate the field.
184
- agent_name = (
185
- metadata.get('agent_name')
186
- or default_agent_name
187
- or self.DEFAULT_AGENT_NAME
188
- )
189
-
190
- for score_entry in scores:
191
- record = {
192
- 'agent_name': agent_name,
193
- 'agent_version': metadata.get('agent_version', 'Unknown'),
194
- 'llm_base': metadata.get('model', 'unknown'),
195
- 'openness': metadata.get('openness', 'unknown'),
196
- 'submission_time': score_entry.get('submission_time', metadata.get('submission_time', '')),
197
- 'release_date': metadata.get('release_date', ''),
198
- 'parameter_count_b': metadata.get('parameter_count_b'),
199
- 'active_parameter_count_b': metadata.get('active_parameter_count_b'),
200
- 'score': score_entry.get('score'),
201
- 'metric': score_entry.get('metric', 'unknown'),
202
- 'cost_per_instance': score_entry.get('cost_per_instance'),
203
- 'average_runtime': score_entry.get('average_runtime'),
204
- 'tags': [score_entry.get('benchmark')],
205
- 'full_archive': score_entry.get('full_archive', ''),
206
- 'eval_visualization_page': score_entry.get('eval_visualization_page', ''),
207
- }
208
- records.append(record)
209
- return records, errors
210
-
211
  def _load_from_agent_dirs(self):
212
- """Load agent records based on ``self.agent_filter``.
213
-
214
- - ``"openhands"`` (default): only ``{config}/results/{model}/``,
215
- which is the canonical OpenHands leaderboard. The Home page and
216
- the per-category subpages use this.
217
- - ``"alternative"``: only
218
- ``{config}/alternative_agents/{type}/{model}/`` (acp-claude,
219
- acp-codex, acp-gemini, openhands_subagents, ...). The dedicated
220
- Alternative Agents page uses this.
221
-
222
- Returns ``None`` if no records were found (which makes the caller
223
- render an empty-state placeholder).
224
- """
225
  all_records = []
226
  all_validation_errors = []
227
-
228
- if self.agent_filter == self.AGENT_FILTER_OPENHANDS:
229
- # Default OpenHands agent results
230
- results_dir = self.config_path / "results"
231
- if results_dir.exists():
232
- for agent_dir in results_dir.iterdir():
233
- if not agent_dir.is_dir():
234
- continue
235
- records, errors = self._records_from_agent_dir(agent_dir)
236
- all_records.extend(records)
237
- all_validation_errors.extend(errors)
238
- else:
239
- # Alternative agents (one subdirectory per agent_type, then per model)
240
- # Default agent_name per agent_type matches the AGENT_NAME_BY_TYPE
241
- # map in OpenHands/evaluation push_to_index_from_archive.py — keeping
242
- # it in sync ensures rows are labelled the same way the index repo
243
- # records them.
244
- agent_type_default_name = {
245
- 'acp-claude': 'Claude Code',
246
- 'acp-codex': 'Codex',
247
- 'acp-gemini': 'Gemini CLI',
248
- }
249
- alt_dir = self.config_path / "alternative_agents"
250
- if alt_dir.exists():
251
- for type_dir in alt_dir.iterdir():
252
- if not type_dir.is_dir():
253
- continue
254
- default_name = agent_type_default_name.get(type_dir.name)
255
- if default_name is None:
256
- continue # skip unlisted agent types (e.g. openhands_subagents)
257
- for agent_dir in type_dir.iterdir():
258
- if not agent_dir.is_dir():
259
- continue
260
- records, errors = self._records_from_agent_dir(
261
- agent_dir, default_agent_name=default_name
262
- )
263
- all_records.extend(records)
264
- all_validation_errors.extend(errors)
265
-
 
266
  # Log validation errors if any
267
  if all_validation_errors:
268
  logger.warning(f"Schema validation errors ({len(all_validation_errors)} total):")
@@ -270,10 +183,10 @@ class SimpleLeaderboardViewer:
270
  logger.warning(f" - {error}")
271
  if len(all_validation_errors) > 5:
272
  logger.warning(f" ... and {len(all_validation_errors) - 5} more")
273
-
274
  if not all_records:
275
- return None # Caller will render empty-state placeholder
276
-
277
  return pd.DataFrame(all_records)
278
 
279
  def _load(self):
@@ -293,36 +206,26 @@ class SimpleLeaderboardViewer:
293
  # Group by agent (version + model combination) to aggregate results across datasets
294
  transformed_records = []
295
 
296
- # Create a unique identifier per (agent_name, agent_version, model)
297
- # tuple. Including agent_name keeps an OpenHands run and a Claude
298
- # Code run on the same SDK version + model from collapsing into
299
- # one row when both submit to the leaderboard.
300
- df['agent_name'] = df['agent_name'].fillna(self.DEFAULT_AGENT_NAME)
301
- df['agent_id'] = (
302
- df['agent_name'].astype(str)
303
- + '_' + df['agent_version'].astype(str)
304
- + '_' + df['llm_base'].astype(str)
305
- )
306
-
307
  for agent_id in df['agent_id'].unique():
308
  agent_records = df[df['agent_id'] == agent_id]
309
-
310
  # Build a single record for this agent
311
  first_record = agent_records.iloc[0]
312
  agent_version = first_record['agent_version']
313
- agent_name = first_record['agent_name']
314
-
315
  # Normalize openness to "open" or "closed"
316
  from aliases import OPENNESS_MAPPING
317
  raw_openness = first_record['openness']
318
  normalized_openness = OPENNESS_MAPPING.get(raw_openness, raw_openness)
319
-
320
  # All 5 categories for the leaderboard
321
  ALL_CATEGORIES = ['Issue Resolution', 'Frontend', 'Greenfield', 'Testing', 'Information Gathering']
322
-
323
  record = {
324
  # Core agent info - use final display names
325
- 'agent_name': agent_name, # Will become "Agent"
326
  'SDK version': agent_version, # Will become "SDK Version"
327
  'Language model': first_record['llm_base'], # Will become "Language Model"
328
  'openness': normalized_openness, # Will become "Openness" (simplified to "open" or "closed")
@@ -332,7 +235,7 @@ class SimpleLeaderboardViewer:
332
  'parameter_count_b': first_record.get('parameter_count_b'), # Total params in billions
333
  'active_parameter_count_b': first_record.get('active_parameter_count_b'), # Active params for MoE
334
  # Additional columns expected by the transformer
335
- # Use agent_id (name_version_model) as unique identifier for Pareto frontier calculation
336
  'id': agent_id,
337
  'source': first_record.get('source', ''), # Will become "Source"
338
  'logs': first_record.get('logs', ''), # Will become "Logs"
 
96
 
97
  class SimpleLeaderboardViewer:
98
  """Simple replacement for agent-eval's LeaderboardViewer."""
99
+
100
+ def __init__(self, data_dir: str, config: str, split: str):
 
 
 
 
 
 
 
 
 
101
  """
102
  Args:
103
  data_dir: Path to data directory
104
  config: Config name (e.g., "1.0.0-dev1")
105
  split: Split name (e.g., "validation" or "test")
 
 
 
 
 
 
 
 
 
 
106
  """
 
 
 
 
 
 
107
  self.data_dir = Path(data_dir)
108
  self.config = config
109
  self.split = split
 
110
  self.config_path = self.data_dir / config
111
 
112
  # Benchmark to category mappings (single source of truth)
 
127
  if benchmark not in self.tag_map[category]:
128
  self.tag_map[category].append(benchmark)
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  def _load_from_agent_dirs(self):
131
+ """Load data from new agent-centric directory structure (results/YYYYMMDD_model/)."""
132
+ results_dir = self.config_path / "results"
133
+
134
+ if not results_dir.exists():
135
+ return None # Fall back to old format
136
+
 
 
 
 
 
 
 
137
  all_records = []
138
  all_validation_errors = []
139
+
140
+ # Iterate through each agent directory
141
+ for agent_dir in results_dir.iterdir():
142
+ if not agent_dir.is_dir():
143
+ continue
144
+
145
+ # Load and validate using pydantic models
146
+ metadata, scores, errors = load_and_validate_agent_data(agent_dir)
147
+
148
+ if errors:
149
+ all_validation_errors.extend(errors)
150
+
151
+ if metadata is None or scores is None:
152
+ continue
153
+
154
+ # Skip entries that are hidden from the leaderboard
155
+ if metadata.get('hide_from_leaderboard', False):
156
+ logger.info(f"Skipping {agent_dir.name}: hide_from_leaderboard is True")
157
+ continue
158
+
159
+ # Create one record per benchmark (mimicking old JSONL format)
160
+ for score_entry in scores:
161
+ record = {
162
+ 'agent_version': metadata.get('agent_version', 'Unknown'),
163
+ 'llm_base': metadata.get('model', 'unknown'),
164
+ 'openness': metadata.get('openness', 'unknown'),
165
+ 'submission_time': metadata.get('submission_time', ''),
166
+ 'release_date': metadata.get('release_date', ''), # Model release date
167
+ 'parameter_count_b': metadata.get('parameter_count_b'), # Total params in billions
168
+ 'active_parameter_count_b': metadata.get('active_parameter_count_b'), # Active params for MoE
169
+ 'score': score_entry.get('score'),
170
+ 'metric': score_entry.get('metric', 'unknown'),
171
+ 'cost_per_instance': score_entry.get('cost_per_instance'),
172
+ 'average_runtime': score_entry.get('average_runtime'),
173
+ 'tags': [score_entry.get('benchmark')],
174
+ 'full_archive': score_entry.get('full_archive', ''), # Download URL for trajectories
175
+ 'eval_visualization_page': score_entry.get('eval_visualization_page', ''), # Laminar visualization URL
176
+ }
177
+ all_records.append(record)
178
+
179
  # Log validation errors if any
180
  if all_validation_errors:
181
  logger.warning(f"Schema validation errors ({len(all_validation_errors)} total):")
 
183
  logger.warning(f" - {error}")
184
  if len(all_validation_errors) > 5:
185
  logger.warning(f" ... and {len(all_validation_errors) - 5} more")
186
+
187
  if not all_records:
188
+ return None # Fall back to old format
189
+
190
  return pd.DataFrame(all_records)
191
 
192
  def _load(self):
 
206
  # Group by agent (version + model combination) to aggregate results across datasets
207
  transformed_records = []
208
 
209
+ # Create a unique identifier for each agent (version + model)
210
+ df['agent_id'] = df['agent_version'] + '_' + df['llm_base']
211
+
 
 
 
 
 
 
 
 
212
  for agent_id in df['agent_id'].unique():
213
  agent_records = df[df['agent_id'] == agent_id]
214
+
215
  # Build a single record for this agent
216
  first_record = agent_records.iloc[0]
217
  agent_version = first_record['agent_version']
218
+
 
219
  # Normalize openness to "open" or "closed"
220
  from aliases import OPENNESS_MAPPING
221
  raw_openness = first_record['openness']
222
  normalized_openness = OPENNESS_MAPPING.get(raw_openness, raw_openness)
223
+
224
  # All 5 categories for the leaderboard
225
  ALL_CATEGORIES = ['Issue Resolution', 'Frontend', 'Greenfield', 'Testing', 'Information Gathering']
226
+
227
  record = {
228
  # Core agent info - use final display names
 
229
  'SDK version': agent_version, # Will become "SDK Version"
230
  'Language model': first_record['llm_base'], # Will become "Language Model"
231
  'openness': normalized_openness, # Will become "Openness" (simplified to "open" or "closed")
 
235
  'parameter_count_b': first_record.get('parameter_count_b'), # Total params in billions
236
  'active_parameter_count_b': first_record.get('active_parameter_count_b'), # Active params for MoE
237
  # Additional columns expected by the transformer
238
+ # Use agent_id (version_model) as unique identifier for Pareto frontier calculation
239
  'id': agent_id,
240
  'source': first_record.get('source', ''), # Will become "Source"
241
  'logs': first_record.get('logs', ''), # Will become "Logs"
tests/test_runtime_sorting.py DELETED
@@ -1,40 +0,0 @@
1
- import pandas as pd
2
-
3
- from leaderboard_transformer import format_runtime_column
4
-
5
-
6
- def test_runtime_strings_sort_numerically_in_ascending_order():
7
- df = pd.DataFrame(
8
- {
9
- "Average Score": [0.8, 0.8, 0.8, 0.8, None],
10
- "Average Runtime": [1323.0, 372.0, 410.0, None, None],
11
- }
12
- )
13
-
14
- formatted = format_runtime_column(df.copy(), "Average Runtime")
15
- runtimes = formatted["Average Runtime"].tolist()
16
-
17
- assert sorted(runtimes) == [
18
- runtimes[1],
19
- runtimes[2],
20
- runtimes[0],
21
- runtimes[3],
22
- runtimes[4],
23
- ]
24
-
25
-
26
- def test_runtime_formatting_preserves_visible_labels():
27
- df = pd.DataFrame(
28
- {
29
- "Average Score": [0.8, 0.8, None],
30
- "Average Runtime": [45.2, None, None],
31
- }
32
- )
33
-
34
- formatted = format_runtime_column(df.copy(), "Average Runtime")
35
- values = formatted["Average Runtime"].tolist()
36
-
37
- assert values[0].endswith("45s")
38
- assert values[1].endswith("Missing</span>")
39
- assert values[2].endswith("Not Submitted</span>")
40
- assert 'display:none' in values[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ui_components.py CHANGED
@@ -508,36 +508,28 @@ class DummyViewer:
508
  # The _load method returns the error DataFrame and an empty tag map
509
  return self._error_df, {}
510
 
511
- def get_leaderboard_viewer_instance(
512
- split: str,
513
- agent_filter: str = SimpleLeaderboardViewer.AGENT_FILTER_OPENHANDS,
514
- ):
515
  """
516
- Fetches the LeaderboardViewer for a (split, agent_filter) pair, using a
517
- thread-safe cache to avoid re-downloading data. The cache is keyed on
518
- both axes so the OpenHands and Alternative Agents pages don't fight
519
- over a single slot. On error, returns a stable DummyViewer.
520
  """
521
  global CACHED_VIEWERS, CACHED_TAG_MAPS
522
 
523
- cache_key = (split, agent_filter)
524
-
525
  with _cache_lock:
526
- if cache_key in CACHED_VIEWERS:
527
  # Cache hit: return the cached viewer and tag map
528
- return CACHED_VIEWERS[cache_key], CACHED_TAG_MAPS.get(cache_key, {"Overall": []})
529
 
530
  # --- Cache miss: try to load data from the source ---
531
  try:
532
  # First try to load from extracted data directory (local mock data)
533
  data_dir = EXTRACTED_DATA_DIR if os.path.exists(EXTRACTED_DATA_DIR) else "mock_results"
534
-
535
- print(f"Loading data for split '{split}' (agent_filter={agent_filter}) from: {data_dir}/{CONFIG_NAME}")
536
  viewer = SimpleLeaderboardViewer(
537
  data_dir=data_dir,
538
  config=CONFIG_NAME,
539
- split=split,
540
- agent_filter=agent_filter,
541
  )
542
 
543
  # Simplify tag map creation
@@ -545,14 +537,14 @@ def get_leaderboard_viewer_instance(
545
 
546
  # Cache the results for next time (thread-safe)
547
  with _cache_lock:
548
- CACHED_VIEWERS[cache_key] = viewer
549
- CACHED_TAG_MAPS[cache_key] = pretty_tag_map # Cache the pretty map directly
550
 
551
  return viewer, pretty_tag_map
552
 
553
  except Exception as e:
554
  # On ANY error, create a consistent error message and cache a DummyViewer
555
- error_message = f"Error loading data for split '{split}' (agent_filter={agent_filter}): {e}"
556
  print(format_error(error_message))
557
 
558
  dummy_df = pd.DataFrame({"Message": [error_message]})
@@ -561,8 +553,8 @@ def get_leaderboard_viewer_instance(
561
 
562
  # Cache the dummy objects so we don't try to fetch again on this run
563
  with _cache_lock:
564
- CACHED_VIEWERS[cache_key] = dummy_viewer
565
- CACHED_TAG_MAPS[cache_key] = dummy_tag_map
566
 
567
  return dummy_viewer, dummy_tag_map
568
 
@@ -705,7 +697,7 @@ def create_leaderboard_display(
705
  primary_runtime_col = f"{category_name} Runtime"
706
 
707
  # Function to create cost/performance scatter plot from data
708
- def create_cost_scatter_plot(df_data, mark_by=MARK_BY_DEFAULT, show_all_labels=False):
709
  return _plot_scatter_plotly(
710
  data=df_data,
711
  x=primary_cost_col if primary_cost_col in df_data.columns else None,
@@ -713,12 +705,11 @@ def create_leaderboard_display(
713
  agent_col="SDK Version",
714
  name=category_name,
715
  plot_type='cost',
716
- mark_by=mark_by,
717
- show_all_labels=show_all_labels
718
  )
719
 
720
  # Function to create runtime/performance scatter plot from data
721
- def create_runtime_scatter_plot(df_data, mark_by=MARK_BY_DEFAULT, show_all_labels=False):
722
  return _plot_scatter_plotly(
723
  data=df_data,
724
  x=primary_runtime_col if primary_runtime_col in df_data.columns else None,
@@ -726,8 +717,7 @@ def create_leaderboard_display(
726
  agent_col="SDK Version",
727
  name=category_name,
728
  plot_type='runtime',
729
- mark_by=mark_by,
730
- show_all_labels=show_all_labels
731
  )
732
 
733
  # Create initial cost scatter plots for all filter combinations
@@ -794,13 +784,6 @@ def create_leaderboard_display(
794
  )
795
  else:
796
  show_open_only_checkbox = None
797
-
798
- # Add checkbox for showing all labels on scatter plot
799
- show_all_labels_checkbox = gr.Checkbox(
800
- label="Show all labels on scatter plots",
801
- value=False,
802
- elem_id="show-all-labels-toggle"
803
- )
804
 
805
  with gr.Column(scale=1):
806
  mark_by_dropdown = gr.Dropdown(
@@ -844,7 +827,7 @@ def create_leaderboard_display(
844
  )
845
 
846
  # Update function for filters - handles checkboxes and mark_by dropdown
847
- def update_display(show_incomplete, show_open_only, mark_by, show_all_labels):
848
  # Determine which dataframe to show based on checkbox states
849
  if show_open_only:
850
  df_to_show = df_display_open if show_incomplete else df_display_complete_open
@@ -853,9 +836,9 @@ def create_leaderboard_display(
853
  df_to_show = df_display_all if show_incomplete else df_display_complete
854
  view_df = df_view_full if show_incomplete else df_view_complete
855
 
856
- # Regenerate plots with current mark_by and show_all_labels settings
857
- cost_plot = create_cost_scatter_plot(view_df, mark_by, show_all_labels)
858
- runtime_plot = create_runtime_scatter_plot(view_df, mark_by, show_all_labels)
859
  return df_to_show, cost_plot, runtime_plot
860
 
861
  # Connect checkboxes and dropdown to the update function
@@ -866,7 +849,6 @@ def create_leaderboard_display(
866
  # Add a dummy value for show_open_only when checkbox doesn't exist
867
  filter_inputs = [show_incomplete_checkbox, gr.State(value=False)]
868
  filter_inputs.append(mark_by_dropdown)
869
- filter_inputs.append(show_all_labels_checkbox)
870
 
871
  show_incomplete_checkbox.change(
872
  fn=update_display,
@@ -884,11 +866,6 @@ def create_leaderboard_display(
884
  inputs=filter_inputs,
885
  outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
886
  )
887
- show_all_labels_checkbox.change(
888
- fn=update_display,
889
- inputs=filter_inputs,
890
- outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
891
- )
892
  else:
893
  dataframe_component = gr.DataFrame(
894
  headers=df_headers,
@@ -903,15 +880,15 @@ def create_leaderboard_display(
903
  )
904
 
905
  # Update function for mark_by and optional open_only checkbox
906
- def update_display_no_complete(show_open_only, mark_by, show_all_labels):
907
  if show_open_only:
908
  df_to_show = df_display_open
909
  view_df = df_view_open
910
  else:
911
  df_to_show = df_display_all
912
  view_df = df_view_full
913
- cost_plot = create_cost_scatter_plot(view_df, mark_by, show_all_labels)
914
- runtime_plot = create_runtime_scatter_plot(view_df, mark_by, show_all_labels)
915
  return df_to_show, cost_plot, runtime_plot
916
 
917
  filter_inputs_no_complete = []
@@ -920,7 +897,6 @@ def create_leaderboard_display(
920
  else:
921
  filter_inputs_no_complete.append(gr.State(value=False))
922
  filter_inputs_no_complete.append(mark_by_dropdown)
923
- filter_inputs_no_complete.append(show_all_labels_checkbox)
924
 
925
  if show_open_only_checkbox is not None:
926
  show_open_only_checkbox.change(
@@ -933,18 +909,13 @@ def create_leaderboard_display(
933
  inputs=filter_inputs_no_complete,
934
  outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
935
  )
936
- show_all_labels_checkbox.change(
937
- fn=update_display_no_complete,
938
- inputs=filter_inputs_no_complete,
939
- outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
940
- )
941
 
942
  legend_markdown = create_legend_markdown(category_name)
943
  gr.HTML(value=legend_markdown, elem_id="legend-markdown")
944
 
945
  # Add a timer to periodically check for data updates and refresh the UI
946
  # This runs every 60 seconds to check if new data is available
947
- def check_and_refresh_data(show_incomplete, show_open_only=False, mark_by=MARK_BY_DEFAULT, show_all_labels=False):
948
  """Check if data has been refreshed and return updated data if so."""
949
  current_version = get_data_version()
950
  if current_version > initial_data_version:
@@ -954,7 +925,7 @@ def create_leaderboard_display(
954
  if not new_df.empty:
955
  new_transformer = DataTransformer(new_df, new_tag_map)
956
  new_df_view_full, _ = new_transformer.view(tag=category_name, use_plotly=True)
957
-
958
  # Prepare both complete and all entries versions
959
  if 'Categories Attempted' in new_df_view_full.columns:
960
  new_df_view_complete = new_df_view_full[new_df_view_full['Categories Attempted'] == '5/5'].copy()
@@ -974,16 +945,16 @@ def create_leaderboard_display(
974
  new_df_display_open = prepare_df_for_display(new_df_view_open)
975
  new_df_display_complete_open = prepare_df_for_display(new_df_view_complete_open)
976
 
977
- # Create new scatter plots for all combinations (with current mark_by and show_all_labels)
978
- new_cost_scatter_complete = create_cost_scatter_plot(new_df_view_complete, mark_by, show_all_labels) if len(new_df_display_complete) > 0 else go.Figure()
979
- new_cost_scatter_all = create_cost_scatter_plot(new_df_view_full, mark_by, show_all_labels)
980
- new_cost_scatter_open = create_cost_scatter_plot(new_df_view_open, mark_by, show_all_labels) if len(new_df_view_open) > 0 else go.Figure()
981
- new_cost_scatter_complete_open = create_cost_scatter_plot(new_df_view_complete_open, mark_by, show_all_labels) if len(new_df_view_complete_open) > 0 else go.Figure()
982
 
983
- new_runtime_scatter_complete = create_runtime_scatter_plot(new_df_view_complete, mark_by, show_all_labels) if len(new_df_display_complete) > 0 else go.Figure()
984
- new_runtime_scatter_all = create_runtime_scatter_plot(new_df_view_full, mark_by, show_all_labels)
985
- new_runtime_scatter_open = create_runtime_scatter_plot(new_df_view_open, mark_by, show_all_labels) if len(new_df_view_open) > 0 else go.Figure()
986
- new_runtime_scatter_complete_open = create_runtime_scatter_plot(new_df_view_complete_open, mark_by, show_all_labels) if len(new_df_view_complete_open) > 0 else go.Figure()
987
 
988
  # Return the appropriate data based on checkbox states
989
  if show_open_only:
@@ -1014,25 +985,18 @@ def create_leaderboard_display(
1014
 
1015
  # Connect the timer to the refresh function
1016
  if show_incomplete_checkbox is not None:
 
1017
  if show_open_only_checkbox is not None:
1018
- refresh_timer.tick(
1019
- fn=check_and_refresh_data,
1020
- inputs=[show_incomplete_checkbox, show_open_only_checkbox, mark_by_dropdown, show_all_labels_checkbox],
1021
- outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
1022
- )
1023
- else:
1024
- # No open/closed split in this dataset — gr.State can't fill the gap in a
1025
- # timer tick (no session context), so use a wrapper that fixes show_open_only=False.
1026
- def _timer_refresh_no_open(show_incomplete, mark_by, show_all_labels):
1027
- return check_and_refresh_data(show_incomplete, False, mark_by, show_all_labels)
1028
- refresh_timer.tick(
1029
- fn=_timer_refresh_no_open,
1030
- inputs=[show_incomplete_checkbox, mark_by_dropdown, show_all_labels_checkbox],
1031
- outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
1032
- )
1033
  else:
1034
  # If no incomplete checkbox, always show all data (but still filter by open if needed)
1035
- def check_and_refresh_all(show_open_only=False, mark_by=MARK_BY_DEFAULT, show_all_labels=False):
1036
  current_version = get_data_version()
1037
  if current_version > initial_data_version:
1038
  print(f"[REFRESH] Data version changed, reloading...")
@@ -1045,8 +1009,8 @@ def create_leaderboard_display(
1045
  new_df_view_full = new_df_view_full[new_df_view_full['Openness'].str.lower() == 'open'].copy()
1046
 
1047
  new_df_display_all = prepare_df_for_display(new_df_view_full)
1048
- new_cost_scatter_all = create_cost_scatter_plot(new_df_view_full, mark_by, show_all_labels)
1049
- new_runtime_scatter_all = create_runtime_scatter_plot(new_df_view_full, mark_by, show_all_labels)
1050
  return new_df_display_all, new_cost_scatter_all, new_runtime_scatter_all
1051
 
1052
  if show_open_only:
@@ -1056,20 +1020,20 @@ def create_leaderboard_display(
1056
  if show_open_only_checkbox is not None:
1057
  refresh_timer.tick(
1058
  fn=check_and_refresh_all,
1059
- inputs=[show_open_only_checkbox, mark_by_dropdown, show_all_labels_checkbox],
1060
  outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
1061
  )
1062
  else:
1063
- def check_and_refresh_simple(mark_by=MARK_BY_DEFAULT, show_all_labels=False):
1064
- return check_and_refresh_all(False, mark_by, show_all_labels)
1065
  refresh_timer.tick(
1066
  fn=check_and_refresh_simple,
1067
- inputs=[mark_by_dropdown, show_all_labels_checkbox],
1068
  outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
1069
  )
1070
 
1071
- # Return the filter controls so they can be used to update other sections
1072
- return show_incomplete_checkbox, show_open_only_checkbox, mark_by_dropdown
1073
 
1074
  # # --- Detailed Benchmark Display ---
1075
  def create_benchmark_details_display(
@@ -1304,17 +1268,12 @@ def create_benchmark_details_display(
1304
  legend_markdown = create_legend_markdown(benchmark_name)
1305
  gr.HTML(value=legend_markdown, elem_id="legend-markdown")
1306
 
1307
- def get_full_leaderboard_data(
1308
- split: str,
1309
- agent_filter: str = SimpleLeaderboardViewer.AGENT_FILTER_OPENHANDS,
1310
- ) -> tuple[pd.DataFrame, dict]:
1311
  """
1312
- Loads and transforms the complete dataset for a (split, agent_filter)
1313
- pair. ``agent_filter`` defaults to ``"openhands"`` so existing pages
1314
- that don't pass it stay on the canonical leaderboard. The Alternative
1315
- Agents page passes ``"alternative"`` to get the third-party harnesses.
1316
  """
1317
- viewer_or_data, raw_tag_map = get_leaderboard_viewer_instance(split, agent_filter=agent_filter)
1318
 
1319
  if isinstance(viewer_or_data, (SimpleLeaderboardViewer, DummyViewer)):
1320
  raw_df, _ = viewer_or_data._load()
 
508
  # The _load method returns the error DataFrame and an empty tag map
509
  return self._error_df, {}
510
 
511
+ def get_leaderboard_viewer_instance(split: str):
 
 
 
512
  """
513
+ Fetches the LeaderboardViewer for a split, using a thread-safe cache to avoid
514
+ re-downloading data. On error, returns a stable DummyViewer object.
 
 
515
  """
516
  global CACHED_VIEWERS, CACHED_TAG_MAPS
517
 
 
 
518
  with _cache_lock:
519
+ if split in CACHED_VIEWERS:
520
  # Cache hit: return the cached viewer and tag map
521
+ return CACHED_VIEWERS[split], CACHED_TAG_MAPS.get(split, {"Overall": []})
522
 
523
  # --- Cache miss: try to load data from the source ---
524
  try:
525
  # First try to load from extracted data directory (local mock data)
526
  data_dir = EXTRACTED_DATA_DIR if os.path.exists(EXTRACTED_DATA_DIR) else "mock_results"
527
+
528
+ print(f"Loading data for split '{split}' from: {data_dir}/{CONFIG_NAME}")
529
  viewer = SimpleLeaderboardViewer(
530
  data_dir=data_dir,
531
  config=CONFIG_NAME,
532
+ split=split
 
533
  )
534
 
535
  # Simplify tag map creation
 
537
 
538
  # Cache the results for next time (thread-safe)
539
  with _cache_lock:
540
+ CACHED_VIEWERS[split] = viewer
541
+ CACHED_TAG_MAPS[split] = pretty_tag_map # Cache the pretty map directly
542
 
543
  return viewer, pretty_tag_map
544
 
545
  except Exception as e:
546
  # On ANY error, create a consistent error message and cache a DummyViewer
547
+ error_message = f"Error loading data for split '{split}': {e}"
548
  print(format_error(error_message))
549
 
550
  dummy_df = pd.DataFrame({"Message": [error_message]})
 
553
 
554
  # Cache the dummy objects so we don't try to fetch again on this run
555
  with _cache_lock:
556
+ CACHED_VIEWERS[split] = dummy_viewer
557
+ CACHED_TAG_MAPS[split] = dummy_tag_map
558
 
559
  return dummy_viewer, dummy_tag_map
560
 
 
697
  primary_runtime_col = f"{category_name} Runtime"
698
 
699
  # Function to create cost/performance scatter plot from data
700
+ def create_cost_scatter_plot(df_data, mark_by=MARK_BY_DEFAULT):
701
  return _plot_scatter_plotly(
702
  data=df_data,
703
  x=primary_cost_col if primary_cost_col in df_data.columns else None,
 
705
  agent_col="SDK Version",
706
  name=category_name,
707
  plot_type='cost',
708
+ mark_by=mark_by
 
709
  )
710
 
711
  # Function to create runtime/performance scatter plot from data
712
+ def create_runtime_scatter_plot(df_data, mark_by=MARK_BY_DEFAULT):
713
  return _plot_scatter_plotly(
714
  data=df_data,
715
  x=primary_runtime_col if primary_runtime_col in df_data.columns else None,
 
717
  agent_col="SDK Version",
718
  name=category_name,
719
  plot_type='runtime',
720
+ mark_by=mark_by
 
721
  )
722
 
723
  # Create initial cost scatter plots for all filter combinations
 
784
  )
785
  else:
786
  show_open_only_checkbox = None
 
 
 
 
 
 
 
787
 
788
  with gr.Column(scale=1):
789
  mark_by_dropdown = gr.Dropdown(
 
827
  )
828
 
829
  # Update function for filters - handles checkboxes and mark_by dropdown
830
+ def update_display(show_incomplete, show_open_only, mark_by):
831
  # Determine which dataframe to show based on checkbox states
832
  if show_open_only:
833
  df_to_show = df_display_open if show_incomplete else df_display_complete_open
 
836
  df_to_show = df_display_all if show_incomplete else df_display_complete
837
  view_df = df_view_full if show_incomplete else df_view_complete
838
 
839
+ # Regenerate plots with current mark_by setting
840
+ cost_plot = create_cost_scatter_plot(view_df, mark_by)
841
+ runtime_plot = create_runtime_scatter_plot(view_df, mark_by)
842
  return df_to_show, cost_plot, runtime_plot
843
 
844
  # Connect checkboxes and dropdown to the update function
 
849
  # Add a dummy value for show_open_only when checkbox doesn't exist
850
  filter_inputs = [show_incomplete_checkbox, gr.State(value=False)]
851
  filter_inputs.append(mark_by_dropdown)
 
852
 
853
  show_incomplete_checkbox.change(
854
  fn=update_display,
 
866
  inputs=filter_inputs,
867
  outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
868
  )
 
 
 
 
 
869
  else:
870
  dataframe_component = gr.DataFrame(
871
  headers=df_headers,
 
880
  )
881
 
882
  # Update function for mark_by and optional open_only checkbox
883
+ def update_display_no_complete(show_open_only, mark_by):
884
  if show_open_only:
885
  df_to_show = df_display_open
886
  view_df = df_view_open
887
  else:
888
  df_to_show = df_display_all
889
  view_df = df_view_full
890
+ cost_plot = create_cost_scatter_plot(view_df, mark_by)
891
+ runtime_plot = create_runtime_scatter_plot(view_df, mark_by)
892
  return df_to_show, cost_plot, runtime_plot
893
 
894
  filter_inputs_no_complete = []
 
897
  else:
898
  filter_inputs_no_complete.append(gr.State(value=False))
899
  filter_inputs_no_complete.append(mark_by_dropdown)
 
900
 
901
  if show_open_only_checkbox is not None:
902
  show_open_only_checkbox.change(
 
909
  inputs=filter_inputs_no_complete,
910
  outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
911
  )
 
 
 
 
 
912
 
913
  legend_markdown = create_legend_markdown(category_name)
914
  gr.HTML(value=legend_markdown, elem_id="legend-markdown")
915
 
916
  # Add a timer to periodically check for data updates and refresh the UI
917
  # This runs every 60 seconds to check if new data is available
918
+ def check_and_refresh_data(show_incomplete, show_open_only=False, mark_by=MARK_BY_DEFAULT):
919
  """Check if data has been refreshed and return updated data if so."""
920
  current_version = get_data_version()
921
  if current_version > initial_data_version:
 
925
  if not new_df.empty:
926
  new_transformer = DataTransformer(new_df, new_tag_map)
927
  new_df_view_full, _ = new_transformer.view(tag=category_name, use_plotly=True)
928
+
929
  # Prepare both complete and all entries versions
930
  if 'Categories Attempted' in new_df_view_full.columns:
931
  new_df_view_complete = new_df_view_full[new_df_view_full['Categories Attempted'] == '5/5'].copy()
 
945
  new_df_display_open = prepare_df_for_display(new_df_view_open)
946
  new_df_display_complete_open = prepare_df_for_display(new_df_view_complete_open)
947
 
948
+ # Create new scatter plots for all combinations (with current mark_by)
949
+ new_cost_scatter_complete = create_cost_scatter_plot(new_df_view_complete, mark_by) if len(new_df_display_complete) > 0 else go.Figure()
950
+ new_cost_scatter_all = create_cost_scatter_plot(new_df_view_full, mark_by)
951
+ new_cost_scatter_open = create_cost_scatter_plot(new_df_view_open, mark_by) if len(new_df_view_open) > 0 else go.Figure()
952
+ new_cost_scatter_complete_open = create_cost_scatter_plot(new_df_view_complete_open, mark_by) if len(new_df_view_complete_open) > 0 else go.Figure()
953
 
954
+ new_runtime_scatter_complete = create_runtime_scatter_plot(new_df_view_complete, mark_by) if len(new_df_display_complete) > 0 else go.Figure()
955
+ new_runtime_scatter_all = create_runtime_scatter_plot(new_df_view_full, mark_by)
956
+ new_runtime_scatter_open = create_runtime_scatter_plot(new_df_view_open, mark_by) if len(new_df_view_open) > 0 else go.Figure()
957
+ new_runtime_scatter_complete_open = create_runtime_scatter_plot(new_df_view_complete_open, mark_by) if len(new_df_view_complete_open) > 0 else go.Figure()
958
 
959
  # Return the appropriate data based on checkbox states
960
  if show_open_only:
 
985
 
986
  # Connect the timer to the refresh function
987
  if show_incomplete_checkbox is not None:
988
+ timer_inputs = [show_incomplete_checkbox]
989
  if show_open_only_checkbox is not None:
990
+ timer_inputs.append(show_open_only_checkbox)
991
+ timer_inputs.append(mark_by_dropdown) # Always include mark_by
992
+ refresh_timer.tick(
993
+ fn=check_and_refresh_data,
994
+ inputs=timer_inputs,
995
+ outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
996
+ )
 
 
 
 
 
 
 
 
997
  else:
998
  # If no incomplete checkbox, always show all data (but still filter by open if needed)
999
+ def check_and_refresh_all(show_open_only=False, mark_by=MARK_BY_DEFAULT):
1000
  current_version = get_data_version()
1001
  if current_version > initial_data_version:
1002
  print(f"[REFRESH] Data version changed, reloading...")
 
1009
  new_df_view_full = new_df_view_full[new_df_view_full['Openness'].str.lower() == 'open'].copy()
1010
 
1011
  new_df_display_all = prepare_df_for_display(new_df_view_full)
1012
+ new_cost_scatter_all = create_cost_scatter_plot(new_df_view_full, mark_by)
1013
+ new_runtime_scatter_all = create_runtime_scatter_plot(new_df_view_full, mark_by)
1014
  return new_df_display_all, new_cost_scatter_all, new_runtime_scatter_all
1015
 
1016
  if show_open_only:
 
1020
  if show_open_only_checkbox is not None:
1021
  refresh_timer.tick(
1022
  fn=check_and_refresh_all,
1023
+ inputs=[show_open_only_checkbox, mark_by_dropdown],
1024
  outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
1025
  )
1026
  else:
1027
+ def check_and_refresh_simple(mark_by=MARK_BY_DEFAULT):
1028
+ return check_and_refresh_all(False, mark_by)
1029
  refresh_timer.tick(
1030
  fn=check_and_refresh_simple,
1031
+ inputs=[mark_by_dropdown],
1032
  outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
1033
  )
1034
 
1035
+ # Return the show_open_only_checkbox and mark_by_dropdown so they can be used to update other sections
1036
+ return show_open_only_checkbox, mark_by_dropdown
1037
 
1038
  # # --- Detailed Benchmark Display ---
1039
  def create_benchmark_details_display(
 
1268
  legend_markdown = create_legend_markdown(benchmark_name)
1269
  gr.HTML(value=legend_markdown, elem_id="legend-markdown")
1270
 
1271
+ def get_full_leaderboard_data(split: str) -> tuple[pd.DataFrame, dict]:
 
 
 
1272
  """
1273
+ Loads and transforms the complete dataset for a given split.
1274
+ This function handles caching and returns the final "pretty" DataFrame and tag map.
 
 
1275
  """
1276
+ viewer_or_data, raw_tag_map = get_leaderboard_viewer_instance(split)
1277
 
1278
  if isinstance(viewer_or_data, (SimpleLeaderboardViewer, DummyViewer)):
1279
  raw_df, _ = viewer_or_data._load()