free-coding-models 0.5.32 → 0.5.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE +43 -21
  2. package/README.md +2 -1
  3. package/changelog/v0.5.33.md +4 -0
  4. package/changelog/v0.5.34.md +4 -0
  5. package/changelog/v0.5.35.md +4 -0
  6. package/package.json +3 -2
  7. package/sources.js +12 -52
  8. package/src/core/router-daemon.js +56 -49
  9. package/src/core/schema-normalizer.js +172 -0
  10. package/src/core/tool-bootstrap.js +11 -0
  11. package/src/core/tool-launchers.js +37 -2
  12. package/src/core/tool-metadata.js +3 -0
  13. package/web/dist/assets/index-B5tTa7_O.js +40 -0
  14. package/web/dist/assets/index-r8niexgJ.css +1 -0
  15. package/web/dist/index.html +2 -2
  16. package/web/src/App.jsx +93 -105
  17. package/web/src/components/changelog/ChangelogView.module.css +13 -14
  18. package/web/src/components/dashboard/FilterBar.jsx +17 -2
  19. package/web/src/components/dashboard/FilterBar.module.css +68 -21
  20. package/web/src/components/dashboard/ModelTable.jsx +118 -42
  21. package/web/src/components/dashboard/ModelTable.module.css +82 -27
  22. package/web/src/components/help/HelpView.module.css +13 -14
  23. package/web/src/components/install/InstallEndpointsView.module.css +13 -13
  24. package/web/src/components/installed/InstalledModelsView.module.css +13 -13
  25. package/web/src/components/layout/Header.jsx +28 -28
  26. package/web/src/components/layout/Header.module.css +56 -30
  27. package/web/src/components/palette/CommandPalette.jsx +14 -14
  28. package/web/src/components/playground/PlaygroundView.module.css +13 -14
  29. package/web/src/components/recommend/RecommendView.module.css +13 -11
  30. package/web/src/components/router/RouterView.module.css +13 -13
  31. package/web/src/components/tools/ToolPicker.module.css +27 -14
  32. package/web/src/global.css +58 -31
  33. package/web/src/hooks/urlState.constants.js +1 -1
  34. package/web/src/hooks/useUrlState.js +19 -3
  35. package/web/dist/assets/index-C_tCF0A5.js +0 -40
  36. package/web/dist/assets/index-CsFt3qt5.css +0 -1
@@ -37,14 +37,14 @@ const PING_MODE_CYCLE = ['speed', 'normal', 'slow', 'forced']
37
37
  // 📖 has a few commands the TUI doesn't). They are appended to the TUI registry
38
38
  // 📖 after a `Web` separator so the two surfaces stay easy to compare.
39
39
  function buildWebEntries(deps) {
40
- const { onCycleTheme, onResetView, onSetPingMode, onOpenChangelog, theme, pingMode, modelsCount, onExport } = deps
40
+ const { onCycleTheme, onResetView, onSetPingMode, theme, pingMode, modelsCount, onExport, onNavigate } = deps
41
41
  const web = [
42
- // Pages (open as modals)
43
- { id: 'page.help', section: 'page', label: 'Open Help', keywords: ['help', 'shortcuts', 'reference'], run: () => deps.onOpenHelp?.() },
44
- { id: 'page.changelog', section: 'page', label: 'Open Changelog', keywords: ['changelog', 'release', 'history', 'version'], run: () => onOpenChangelog?.() },
45
- { id: 'page.playground', section: 'page', label: 'Open Playground', keywords: ['playground', 'chat', 'try', 'prompt', 'router'], run: () => deps.onOpenPlayground?.() },
46
- { id: 'page.install-endpoints', section: 'page', label: 'Open Install Endpoints (M4)', keywords: ['install', 'endpoint', 'tool', 'configure'], disabled: true },
47
- { id: 'page.installed-models', section: 'page', label: 'Open Installed Models (M4)', keywords: ['installed', 'models', 'tools'], disabled: true },
42
+ // Pages
43
+ { id: 'page.help', section: 'page', label: 'Open Help', keywords: ['help', 'shortcuts', 'reference'], run: () => onNavigate?.('help') },
44
+ { id: 'page.changelog', section: 'page', label: 'Open Changelog', keywords: ['changelog', 'release', 'history', 'version'], run: () => onNavigate?.('changelog') },
45
+ { id: 'page.playground', section: 'page', label: 'Open Playground', keywords: ['playground', 'chat', 'try', 'prompt', 'router'], run: () => onNavigate?.('playground') },
46
+ { id: 'page.install-endpoints', section: 'page', label: 'Open Install Endpoints', keywords: ['install', 'endpoint', 'tool', 'configure'], run: () => onNavigate?.('install-endpoints') },
47
+ { id: 'page.installed-models', section: 'page', label: 'Open Installed Models', keywords: ['installed', 'models', 'tools'], run: () => onNavigate?.('installed-models') },
48
48
 
49
49
  // Theme
50
50
  { id: 'action.theme.cycle', section: 'action', label: `Cycle theme (current: ${theme})`, keywords: ['theme', 'dark', 'light', 'auto'], run: onCycleTheme },
@@ -92,7 +92,7 @@ export default function CommandPalette({
92
92
  })
93
93
  const webEntries = buildWebEntries({
94
94
  onCycleTheme, onResetView, onSetPingMode,
95
- onOpenChangelog, onOpenHelp, theme, pingMode, modelsCount: models?.length ?? 0, onExport,
95
+ theme, pingMode, modelsCount: models?.length ?? 0, onExport, onNavigate,
96
96
  })
97
97
  // 📖 Update banner entry (only when an update is available) — mirrors
98
98
  // 📖 the TUI palette's "auto-prepended when newer version known" rule.
@@ -177,12 +177,12 @@ export default function CommandPalette({
177
177
  // 📖 ships in M3), we route the user to the right header menu.
178
178
  const id = item.id
179
179
  if (id === 'open-settings') { onNavigate?.('settings'); onClose(); return }
180
- if (id === 'open-help') { onOpenHelp?.(); onClose(); return }
181
- if (id === 'open-changelog') { onOpenChangelog?.(); onClose(); return }
182
- if (id === 'open-recommend') { onToast?.('Recommend arrives in M3', 'info'); onClose(); return }
183
- if (id === 'open-router-dashboard') { onToast?.('Router dashboard arrives in M4', 'info'); onClose(); return }
184
- if (id === 'open-installed-models') { onToast?.('Installed Models arrives in M4', 'info'); onClose(); return }
185
- if (id === 'open-install-endpoints') { onToast?.('Install Endpoints arrives in M4', 'info'); onClose(); return }
180
+ if (id === 'open-help') { onNavigate?.('help'); onClose(); return }
181
+ if (id === 'open-changelog') { onNavigate?.('changelog'); onClose(); return }
182
+ if (id === 'open-recommend') { onNavigate?.('recommend'); onClose(); return }
183
+ if (id === 'open-router-dashboard') { onNavigate?.('router'); onClose(); return }
184
+ if (id === 'open-installed-models') { onNavigate?.('installed-models'); onClose(); return }
185
+ if (id === 'open-install-endpoints') { onNavigate?.('install-endpoints'); onClose(); return }
186
186
  if (id === 'action-update-now') { onRunUpdate?.(); onClose(); return }
187
187
  if (id === 'action-cycle-theme') { onCycleTheme?.(); onClose(); return }
188
188
  if (id === 'action-reset-view') { onResetView?.(); onClose(); return }
@@ -5,27 +5,26 @@
5
5
  */
6
6
 
7
7
  .overlay {
8
- position: fixed;
9
- inset: 0;
10
- z-index: 200;
11
- background: rgba(0, 0, 0, 0.6);
12
8
  display: flex;
13
- align-items: center;
14
- justify-content: center;
15
- animation: fadeIn 0.15s ease;
9
+ flex-direction: column;
10
+ flex: 1;
11
+ min-height: 0;
12
+ width: 100%;
13
+ padding: 24px;
16
14
  }
17
15
 
18
16
  .modal {
19
- background: var(--bg-primary, #0f0f17);
20
- border: 1px solid var(--border, #1e1e2e);
17
+ background: var(--color-bg-card, #080908);
18
+ border: 1px solid var(--color-border, #161a16);
21
19
  border-radius: 12px;
22
- width: 880px;
23
- max-width: 96vw;
24
- max-height: 90vh;
20
+ width: 100%;
21
+ max-width: 1000px;
22
+ margin: 0 auto;
23
+ flex: 1;
25
24
  display: flex;
26
25
  flex-direction: column;
27
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
28
- animation: slideUp 0.18s ease;
26
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
27
+ min-height: 0;
29
28
  }
30
29
 
31
30
  @keyframes fadeIn {
@@ -1,24 +1,26 @@
1
1
  /** @file web/src/components/recommend/RecommendView.module.css */
2
2
  .backdrop {
3
- position: fixed;
4
- inset: 0;
5
- z-index: 480;
6
3
  display: flex;
7
- align-items: center;
8
- justify-content: center;
4
+ flex-direction: column;
5
+ flex: 1;
6
+ min-height: 0;
7
+ width: 100%;
9
8
  padding: 24px;
10
- background: rgba(0, 0, 0, .64);
11
- backdrop-filter: blur(4px);
12
9
  }
13
10
  .modal {
14
- width: min(980px, 96vw);
15
- max-height: min(820px, 94vh);
11
+ width: 100%;
12
+ max-width: 980px;
13
+ margin: 0 auto;
16
14
  overflow: auto;
17
15
  border: 1px solid var(--color-border);
18
16
  border-radius: 16px;
19
- background: var(--color-bg-elevated);
17
+ background: var(--color-bg-card, #080908);
20
18
  color: var(--color-text);
21
- box-shadow: 0 20px 60px rgba(0,0,0,.48);
19
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
20
+ flex: 1;
21
+ display: flex;
22
+ flex-direction: column;
23
+ min-height: 0;
22
24
  }
23
25
  .header,
24
26
  .footer { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 20px 24px; border-bottom: 1px solid var(--color-border); }
@@ -1,24 +1,24 @@
1
1
  .overlay {
2
- position: fixed;
3
- inset: 0;
4
- z-index: 200;
5
- background: rgba(0, 0, 0, 0.6);
6
2
  display: flex;
7
- align-items: center;
8
- justify-content: center;
9
- animation: fadeIn 0.15s ease;
3
+ flex-direction: column;
4
+ flex: 1;
5
+ min-height: 0;
6
+ width: 100%;
7
+ padding: 24px;
10
8
  }
11
9
 
12
10
  .modal {
13
- background: var(--bg-primary, #0f0f17);
14
- border: 1px solid var(--border, #1e1e2e);
11
+ background: var(--color-bg-card, #080908);
12
+ border: 1px solid var(--color-border, #161a16);
15
13
  border-radius: 12px;
16
- width: 720px;
17
- max-width: 95vw;
18
- max-height: 85vh;
14
+ width: 100%;
15
+ max-width: 1000px;
16
+ margin: 0 auto;
17
+ flex: 1;
19
18
  display: flex;
20
19
  flex-direction: column;
21
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
20
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
21
+ min-height: 0;
22
22
  }
23
23
 
24
24
  .header {
@@ -8,33 +8,46 @@
8
8
  align-items: center;
9
9
  gap: 4px;
10
10
  }
11
- .trigger,
12
- .cycle {
11
+ .trigger {
13
12
  height: 30px;
14
13
  display: inline-flex;
15
14
  align-items: center;
16
15
  justify-content: center;
17
- border: 1px solid var(--color-border);
18
- background: var(--color-surface);
19
- color: var(--color-text);
20
- cursor: pointer;
21
- transition: background 150ms, border-color 150ms, color 150ms;
22
- }
23
- .trigger {
24
16
  gap: 6px;
25
- padding: 0 9px;
17
+ padding: 0 12px;
26
18
  border-radius: 6px;
27
- font-size: 12px;
28
- font-weight: 700;
19
+ font-size: 11px;
20
+ font-weight: 800;
21
+ font-family: var(--font-mono);
22
+ text-transform: uppercase;
23
+ letter-spacing: 0.5px;
24
+ background: var(--color-accent);
25
+ color: var(--color-bg);
26
+ border: 1px solid var(--color-accent);
27
+ cursor: pointer;
28
+ transition: all 150ms var(--ease-out);
29
29
  }
30
30
  .cycle {
31
+ height: 30px;
31
32
  width: 30px;
33
+ display: inline-flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ border: 1px solid var(--color-border);
37
+ background: var(--color-surface);
38
+ color: var(--color-text);
32
39
  border-radius: 6px;
40
+ cursor: pointer;
41
+ transition: all 150ms var(--ease-out);
42
+ }
43
+ .trigger:hover {
44
+ background: var(--color-accent-hover);
45
+ border-color: var(--color-accent-hover);
46
+ color: var(--color-bg);
33
47
  }
34
- .trigger:hover,
35
48
  .cycle:hover {
36
49
  background: var(--color-bg-hover);
37
- border-color: var(--color-accent);
50
+ border-color: var(--color-border-hover);
38
51
  color: var(--color-accent);
39
52
  }
40
53
  .emoji { font-size: 14px; line-height: 1; }
@@ -8,24 +8,24 @@
8
8
 
9
9
  /* ─── CSS Custom Properties (Design Tokens) ─── */
10
10
  :root {
11
- --color-brand: #76b900;
12
- --color-bg: #000000;
13
- --color-bg-elevated: #0a0a0a;
14
- --color-bg-card: #0a0a0a;
15
- --color-bg-hover: #161616;
16
- --color-bg-active: #222222;
17
- --color-surface: #111111;
18
- --color-border: #262626;
19
- --color-border-hover: #404040;
11
+ --color-brand: #c0f20c;
12
+ --color-bg: #050505;
13
+ --color-bg-elevated: #090a09;
14
+ --color-bg-card: #080908;
15
+ --color-bg-hover: #121512;
16
+ --color-bg-active: #181d18;
17
+ --color-surface: #0d100d;
18
+ --color-border: #161a16;
19
+ --color-border-hover: #2e3b2e;
20
20
 
21
21
  --color-text: #ffffff;
22
- --color-text-muted: #888888;
23
- --color-text-dim: #444444;
22
+ --color-text-muted: #8fa08f;
23
+ --color-text-dim: #455245;
24
24
 
25
- --color-accent: #ffffff;
26
- --color-accent-hover: #eaeaea;
27
- --color-accent-glow: rgba(255, 255, 255, 0.1);
28
- --color-accent-dim: rgba(255, 255, 255, 0.05);
25
+ --color-accent: #c0f20c;
26
+ --color-accent-hover: #d2ff2b;
27
+ --color-accent-glow: rgba(192, 242, 12, 0.15);
28
+ --color-accent-dim: rgba(192, 242, 12, 0.08);
29
29
 
30
30
  --color-danger: #ff4444;
31
31
  --color-danger-dim: rgba(255, 68, 68, 0.1);
@@ -160,7 +160,7 @@ textarea:focus-visible {
160
160
  }
161
161
 
162
162
  html {
163
- font-size: 14px;
163
+ font-size: 16px;
164
164
  scroll-behavior: smooth;
165
165
  -webkit-font-smoothing: antialiased;
166
166
  -moz-osx-font-smoothing: grayscale;
@@ -179,23 +179,16 @@ body {
179
179
 
180
180
  a { color: var(--color-accent); text-decoration: none; }
181
181
  a:hover { color: var(--color-accent-hover); }
182
- code { font-family: var(--font-mono); font-size: 12px; background: var(--color-surface); padding: 2px 6px; border-radius: 4px; }
182
+ code { font-family: var(--font-mono); font-size: 14px; background: var(--color-surface); padding: 2px 6px; border-radius: 4px; }
183
183
 
184
184
  /* ─── Background Effects ─── */
185
- .bg-grid {
186
- display: none;
187
- }
188
-
189
- .bg-glow {
190
- display: none;
191
- }
192
-
193
-
194
- [data-theme="light"] .bg-glow { opacity: 0.12; }
195
- [data-theme="light"] .bg-grid {
185
+ [data-theme="light"] .app-shell {
196
186
  background-image:
197
- linear-gradient(rgba(0,0,0,0.04) 1px, transparent 1px),
198
- linear-gradient(90deg, rgba(0,0,0,0.04) 1px, transparent 1px);
187
+ linear-gradient(rgba(0,0,0,0.03) 1px, transparent 1px),
188
+ linear-gradient(90deg, rgba(0,0,0,0.03) 1px, transparent 1px);
189
+ }
190
+ [data-theme="light"] .app-shell::before {
191
+ background: radial-gradient(circle, rgba(0, 143, 77, 0.03) 0%, transparent 70%);
199
192
  }
200
193
 
201
194
  /* ─── App Layout ─── */
@@ -207,6 +200,25 @@ code { font-family: var(--font-mono); font-size: 12px; background: var(--color-s
207
200
  width: 100%;
208
201
  position: relative;
209
202
  z-index: 1;
203
+ background-color: var(--color-bg);
204
+ /* background grid effect */
205
+ background-image:
206
+ linear-gradient(rgba(192, 242, 12, 0.012) 1px, transparent 1px),
207
+ linear-gradient(90deg, rgba(192, 242, 12, 0.012) 1px, transparent 1px);
208
+ background-size: 50px 50px;
209
+ background-position: center top;
210
+ }
211
+
212
+ .app-shell::before {
213
+ content: "";
214
+ position: absolute;
215
+ top: 0;
216
+ left: 25%;
217
+ width: 600px;
218
+ height: 600px;
219
+ background: radial-gradient(circle, rgba(192, 242, 12, 0.03) 0%, transparent 70%);
220
+ z-index: -1;
221
+ pointer-events: none;
210
222
  }
211
223
 
212
224
  .app-content {
@@ -225,6 +237,21 @@ code { font-family: var(--font-mono); font-size: 12px; background: var(--color-s
225
237
  min-height: 0;
226
238
  }
227
239
 
240
+ /* 📖 Dashboard-only: bound the main box to the viewport height (minus the
241
+ 📖 global header) so the model table scrolls INTERNALLY inside .scrollInner
242
+ 📖 instead of growing the whole page. That internal scroll is what lets the
243
+ 📖 green <thead> header be position:sticky and stay pinned while you scroll
244
+ 📖 190+ model rows. Scoped to the dashboard only (main.dashboardView has
245
+ 📖 higher specificity than the shared .view rule, so flex:none + the capped
246
+ 📖 height reliably win). Settings / Analytics / Playground keep their layout. */
247
+ main.dashboardView {
248
+ flex: none;
249
+ height: calc(100vh - var(--header-h));
250
+ height: calc(100dvh - var(--header-h));
251
+ min-height: 0;
252
+ overflow: hidden;
253
+ }
254
+
228
255
  .update-warning-banner {
229
256
  position: sticky;
230
257
  top: var(--header-h, 60px);
@@ -234,7 +261,7 @@ code { font-family: var(--font-mono); font-size: 12px; background: var(--color-s
234
261
  color: #fff;
235
262
  border-bottom: 1px solid rgba(255, 255, 255, 0.22);
236
263
  font-weight: 800;
237
- font-size: 13px;
264
+ font-size: 15px;
238
265
  letter-spacing: 0.01em;
239
266
  text-align: center;
240
267
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
@@ -14,7 +14,7 @@ const SORT_VALUES = new Set([
14
14
  'latestPing', 'avg', 'condition', 'verdict', 'stability', 'uptime',
15
15
  'aiLatency', 'tps', 'trend',
16
16
  ])
17
- const VIEW_VALUES = new Set(['dashboard', 'settings', 'analytics', 'recommend', 'router', 'help', 'changelog'])
17
+ const VIEW_VALUES = new Set(['dashboard', 'settings', 'analytics', 'recommend', 'router', 'help', 'changelog', 'playground', 'install-endpoints', 'installed-models'])
18
18
  const DIR_VALUES = new Set(['asc', 'desc'])
19
19
  const TOOL_MODE_VALUES = new Set(TOOL_MODE_ORDER)
20
20
 
@@ -31,7 +31,17 @@ function parseUrlParams() {
31
31
  if (typeof window === 'undefined') return null
32
32
  const params = new URLSearchParams(window.location.search)
33
33
  const out = {}
34
- if (params.has('view') && VALID_VIEWS.has(params.get('view'))) out.view = params.get('view')
34
+
35
+ // 📖 Read pathname as the view slug
36
+ const pathname = window.location.pathname.replace(/^\/|\/$/g, '')
37
+ if (pathname && VALID_VIEWS.has(pathname)) {
38
+ out.view = pathname
39
+ } else if (params.has('view') && VALID_VIEWS.has(params.get('view'))) {
40
+ out.view = params.get('view')
41
+ } else {
42
+ out.view = 'dashboard'
43
+ }
44
+
35
45
  if (params.has('tier') && VALID_TIERS.has(params.get('tier'))) out.tier = params.get('tier')
36
46
  if (params.has('status') && VALID_STATUS.has(params.get('status'))) out.status = params.get('status')
37
47
  if (params.has('provider')) out.provider = params.get('provider')
@@ -71,10 +81,16 @@ export function buildUrlParams(state) {
71
81
  function writeUrl(state) {
72
82
  if (typeof window === 'undefined') return
73
83
  const params = buildUrlParams(state)
84
+
85
+ // 📖 Extract view from the search params so it becomes the pathname slug instead of a search param!
86
+ const view = params.get('view') || 'dashboard'
87
+ params.delete('view')
88
+
74
89
  const search = params.toString()
90
+ const path = view !== 'dashboard' ? `/${view}` : '/'
75
91
  const newUrl = search
76
- ? `${window.location.pathname}?${search}${window.location.hash}`
77
- : `${window.location.pathname}${window.location.hash}`
92
+ ? `${path}?${search}${window.location.hash}`
93
+ : `${path}${window.location.hash}`
78
94
  // 📖 replaceState (not pushState) so back/forward don't fill up with
79
95
  // 📖 every keystroke. The current URL still updates visibly.
80
96
  window.history.replaceState({}, '', newUrl)