free-coding-models 0.5.86 → 0.5.87

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 (163) hide show
  1. package/README.md +110 -881
  2. package/bin/free-coding-models.js +4 -1
  3. package/changelog/v0.5.87.md +48 -0
  4. package/package.json +8 -5
  5. package/sources.js +4 -4
  6. package/src/core/cache.js +3 -1
  7. package/src/core/config.js +43 -5
  8. package/src/core/endpoint-installer.js +23 -12
  9. package/src/core/installed-models-manager.js +43 -20
  10. package/src/core/kilo-config.js +51 -5
  11. package/src/core/kilo.js +5 -3
  12. package/src/core/legacy-proxy-cleanup.js +22 -5
  13. package/src/core/model-merger.js +4 -0
  14. package/src/core/models-drift.js +9 -0
  15. package/src/core/opencode-config.js +57 -5
  16. package/src/core/opencode.js +67 -41
  17. package/src/core/playground.js +7 -0
  18. package/src/core/probe-cache.js +35 -4
  19. package/src/core/provider-key-tester.js +10 -2
  20. package/src/core/provider-metadata.js +10 -0
  21. package/src/core/provider-quota-fetchers.js +14 -7
  22. package/src/core/router-daemon.js +208 -26
  23. package/src/core/runtime-telemetry.js +27 -1
  24. package/src/core/shared-helpers.js +12 -1
  25. package/src/core/shell-env.js +3 -1
  26. package/src/core/telemetry.js +18 -5
  27. package/src/core/tool-launchers.js +9 -6
  28. package/src/core/updater.js +28 -2
  29. package/src/core/utils.js +76 -71
  30. package/src/tui/app.js +38 -14
  31. package/src/tui/command-palette.js +22 -2
  32. package/src/tui/key-handler.js +123 -179
  33. package/src/tui/overlays.js +21 -71
  34. package/src/tui/render-helpers.js +62 -4
  35. package/src/tui/render-table.js +34 -3
  36. package/src/tui/theme.js +36 -5
  37. package/src/tui/tui-filters.js +28 -8
  38. package/src/tui/tui-state.js +22 -6
  39. package/web/dist/assets/index-BpTpPS0g.js +44 -0
  40. package/web/dist/assets/index-CAzFIt8P.css +1 -0
  41. package/web/dist/index.html +5 -9
  42. package/web/index.html +3 -7
  43. package/web/server.js +97 -23
  44. package/web/vite.config.js +8 -0
  45. package/web/app.legacy.js +0 -900
  46. package/web/dist/assets/index-C5-Ywvv3.css +0 -1
  47. package/web/dist/assets/index-Dm-Xfby9.js +0 -40
  48. package/web/public/favicon.ico +0 -0
  49. package/web/public/favicons/apple-touch-icon.png +0 -0
  50. package/web/public/favicons/browserconfig.xml +0 -12
  51. package/web/public/favicons/favicon-16x16.png +0 -0
  52. package/web/public/favicons/favicon-192x192.png +0 -0
  53. package/web/public/favicons/favicon-32x32.png +0 -0
  54. package/web/public/favicons/favicon-48x48.png +0 -0
  55. package/web/public/favicons/favicon-512x512.png +0 -0
  56. package/web/public/favicons/favicon-96x96.png +0 -0
  57. package/web/public/favicons/favicon.ico +0 -0
  58. package/web/public/favicons/mstile-150x150.png +0 -0
  59. package/web/public/favicons/mstile-310x150.png +0 -0
  60. package/web/public/favicons/mstile-310x310.png +0 -0
  61. package/web/public/favicons/mstile-512x512.png +0 -0
  62. package/web/public/favicons/mstile-70x70.png +0 -0
  63. package/web/public/favicons/site.webmanifest +0 -25
  64. package/web/src/App.jsx +0 -521
  65. package/web/src/components/analytics/AnalyticsView.jsx +0 -147
  66. package/web/src/components/analytics/AnalyticsView.module.css +0 -190
  67. package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
  68. package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
  69. package/web/src/components/atoms/AILatencyCell.jsx +0 -38
  70. package/web/src/components/atoms/AILatencyCell.module.css +0 -51
  71. package/web/src/components/atoms/HealthCell.jsx +0 -143
  72. package/web/src/components/atoms/HealthCell.module.css +0 -60
  73. package/web/src/components/atoms/LastPingCell.jsx +0 -35
  74. package/web/src/components/atoms/LastPingCell.module.css +0 -45
  75. package/web/src/components/atoms/MoodCell.jsx +0 -25
  76. package/web/src/components/atoms/MoodCell.module.css +0 -6
  77. package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
  78. package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
  79. package/web/src/components/atoms/ProviderLogo.jsx +0 -187
  80. package/web/src/components/atoms/ProviderLogo.module.css +0 -103
  81. package/web/src/components/atoms/RankCell.jsx +0 -9
  82. package/web/src/components/atoms/RankCell.module.css +0 -9
  83. package/web/src/components/atoms/Sparkline.jsx +0 -63
  84. package/web/src/components/atoms/StabilityCell.jsx +0 -18
  85. package/web/src/components/atoms/StabilityCell.module.css +0 -13
  86. package/web/src/components/atoms/StatusDot.jsx +0 -13
  87. package/web/src/components/atoms/StatusDot.module.css +0 -29
  88. package/web/src/components/atoms/TPSCell.jsx +0 -36
  89. package/web/src/components/atoms/TPSCell.module.css +0 -44
  90. package/web/src/components/atoms/TierBadge.jsx +0 -10
  91. package/web/src/components/atoms/TierBadge.module.css +0 -29
  92. package/web/src/components/atoms/Toast.jsx +0 -25
  93. package/web/src/components/atoms/Toast.module.css +0 -41
  94. package/web/src/components/atoms/ToastContainer.jsx +0 -16
  95. package/web/src/components/atoms/ToastContainer.module.css +0 -10
  96. package/web/src/components/atoms/VerdictBadge.jsx +0 -36
  97. package/web/src/components/atoms/VerdictBadge.module.css +0 -40
  98. package/web/src/components/changelog/ChangelogView.jsx +0 -135
  99. package/web/src/components/changelog/ChangelogView.module.css +0 -159
  100. package/web/src/components/dashboard/DetailPanel.jsx +0 -217
  101. package/web/src/components/dashboard/DetailPanel.module.css +0 -260
  102. package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
  103. package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
  104. package/web/src/components/dashboard/ExportModal.jsx +0 -84
  105. package/web/src/components/dashboard/ExportModal.module.css +0 -99
  106. package/web/src/components/dashboard/FilterBar.jsx +0 -338
  107. package/web/src/components/dashboard/FilterBar.module.css +0 -342
  108. package/web/src/components/dashboard/ModelTable.jsx +0 -672
  109. package/web/src/components/dashboard/ModelTable.module.css +0 -446
  110. package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
  111. package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
  112. package/web/src/components/help/HelpView.jsx +0 -201
  113. package/web/src/components/help/HelpView.module.css +0 -156
  114. package/web/src/components/install/InstallEndpointsView.jsx +0 -309
  115. package/web/src/components/install/InstallEndpointsView.module.css +0 -427
  116. package/web/src/components/installed/InstalledModelsView.jsx +0 -89
  117. package/web/src/components/installed/InstalledModelsView.module.css +0 -200
  118. package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
  119. package/web/src/components/launch/LaunchButton.jsx +0 -30
  120. package/web/src/components/launch/LaunchButton.module.css +0 -35
  121. package/web/src/components/launch/LaunchModal.module.css +0 -125
  122. package/web/src/components/layout/Footer.jsx +0 -19
  123. package/web/src/components/layout/Footer.module.css +0 -10
  124. package/web/src/components/layout/Header.jsx +0 -265
  125. package/web/src/components/layout/Header.module.css +0 -357
  126. package/web/src/components/palette/CommandPalette.jsx +0 -301
  127. package/web/src/components/palette/CommandPalette.module.css +0 -121
  128. package/web/src/components/playground/PlaygroundChat.jsx +0 -527
  129. package/web/src/components/playground/PlaygroundChat.module.css +0 -382
  130. package/web/src/components/playground/PlaygroundView.jsx +0 -371
  131. package/web/src/components/playground/PlaygroundView.module.css +0 -504
  132. package/web/src/components/recommend/RecommendView.jsx +0 -121
  133. package/web/src/components/recommend/RecommendView.module.css +0 -57
  134. package/web/src/components/router/RouterView.jsx +0 -1212
  135. package/web/src/components/router/RouterView.module.css +0 -1289
  136. package/web/src/components/settings/SettingsView.jsx +0 -555
  137. package/web/src/components/settings/SettingsView.module.css +0 -550
  138. package/web/src/components/tools/ToolPicker.jsx +0 -86
  139. package/web/src/components/tools/ToolPicker.module.css +0 -97
  140. package/web/src/components/update/UpdateChip.jsx +0 -104
  141. package/web/src/components/update/UpdateChip.module.css +0 -146
  142. package/web/src/global.css +0 -298
  143. package/web/src/hooks/urlState.constants.js +0 -28
  144. package/web/src/hooks/useChangelog.js +0 -51
  145. package/web/src/hooks/useColumnSizing.js +0 -211
  146. package/web/src/hooks/useFavorites.js +0 -150
  147. package/web/src/hooks/useFilter.js +0 -310
  148. package/web/src/hooks/useInstalledModels.js +0 -42
  149. package/web/src/hooks/useRecommend.js +0 -67
  150. package/web/src/hooks/useRouterDashboard.js +0 -133
  151. package/web/src/hooks/useSSE.js +0 -73
  152. package/web/src/hooks/useSocket.js +0 -203
  153. package/web/src/hooks/useTheme.js +0 -83
  154. package/web/src/hooks/useTokenUsage.js +0 -103
  155. package/web/src/hooks/useToolMode.js +0 -77
  156. package/web/src/hooks/useUpdateChecker.js +0 -91
  157. package/web/src/hooks/useUrlState.js +0 -170
  158. package/web/src/main.jsx +0 -15
  159. package/web/src/utils/download.js +0 -15
  160. package/web/src/utils/format.js +0 -42
  161. package/web/src/utils/m3.js +0 -57
  162. package/web/src/utils/ranks.js +0 -37
  163. package/web/styles.legacy.css +0 -963
@@ -1,103 +0,0 @@
1
- /**
2
- * @file web/src/hooks/useTokenUsage.js
3
- * @description Hook for Token Usage panel — fetches token data from daemon or file.
4
- * 📖 M4: Returns today's usage, 7-day breakdown, top models, top providers.
5
- *
6
- * @functions useTokenUsage → { data, loading, refresh }
7
- */
8
- import { useState, useEffect, useCallback } from 'react'
9
-
10
- export function useTokenUsage() {
11
- const [data, setData] = useState(null)
12
- const [loading, setLoading] = useState(true)
13
-
14
- const refresh = useCallback(async () => {
15
- setLoading(true)
16
- try {
17
- const resp = await fetch('/api/router/tokens')
18
- const raw = await resp.json()
19
- setData(processTokenData(raw))
20
- } catch {
21
- setData(null)
22
- } finally {
23
- setLoading(false)
24
- }
25
- }, [])
26
-
27
- useEffect(() => { void refresh() }, [refresh])
28
-
29
- return { data, loading, refresh }
30
- }
31
-
32
- function processTokenData(raw) {
33
- if (!raw) return null
34
-
35
- const daily = raw.daily || {}
36
- const allTime = raw.all_time || {}
37
-
38
- // 📖 Build 7-day chart data
39
- const days = []
40
- const today = new Date()
41
- for (let i = 6; i >= 0; i--) {
42
- const d = new Date(today)
43
- d.setDate(d.getDate() - i)
44
- const key = d.toISOString().slice(0, 10)
45
- const entry = daily[key] || { total_tokens: 0, requests: 0 }
46
- days.push({
47
- date: key,
48
- label: d.toLocaleDateString('en', { weekday: 'short' }),
49
- totalTokens: entry.total_tokens || 0,
50
- requests: entry.requests || 0,
51
- })
52
- }
53
-
54
- // 📖 Top models across all tracked days
55
- const modelTotals = {}
56
- for (const entry of Object.values(daily)) {
57
- const byModel = entry.by_model || {}
58
- for (const [key, val] of Object.entries(byModel)) {
59
- if (!modelTotals[key]) modelTotals[key] = { total: 0, requests: 0 }
60
- modelTotals[key].total += val.total || 0
61
- modelTotals[key].requests += val.requests || 0
62
- }
63
- }
64
- const topModels = Object.entries(modelTotals)
65
- .sort((a, b) => b[1].total - a[1].total)
66
- .slice(0, 10)
67
- .map(([key, val]) => ({ key, ...val }))
68
-
69
- // 📖 Top providers (aggregated from model keys)
70
- const providerTotals = {}
71
- for (const [key, val] of Object.entries(modelTotals)) {
72
- const provider = key.split('/')[0]
73
- if (!providerTotals[provider]) providerTotals[provider] = { total: 0, requests: 0 }
74
- providerTotals[provider].total += val.total
75
- providerTotals[provider].requests += val.requests
76
- }
77
- const topProviders = Object.entries(providerTotals)
78
- .sort((a, b) => b[1].total - a[1].total)
79
- .slice(0, 10)
80
- .map(([key, val]) => ({ key, ...val }))
81
-
82
- const todayKey = today.toISOString().slice(0, 10)
83
- const todayData = daily[todayKey] || {}
84
-
85
- return {
86
- today: {
87
- totalTokens: todayData.total_tokens || 0,
88
- promptTokens: todayData.prompt_tokens || 0,
89
- completionTokens: todayData.completion_tokens || 0,
90
- requests: todayData.requests || 0,
91
- },
92
- allTime: {
93
- totalTokens: allTime.total_tokens || 0,
94
- promptTokens: allTime.prompt_tokens || 0,
95
- completionTokens: allTime.completion_tokens || 0,
96
- requests: allTime.requests || 0,
97
- },
98
- sevenDays: days,
99
- topModels,
100
- topProviders,
101
- hasData: allTime.total_tokens > 0 || Object.keys(daily).length > 0,
102
- }
103
- }
@@ -1,77 +0,0 @@
1
- /**
2
- * @file web/src/hooks/useToolMode.js
3
- * @description M3 tool-mode hook for the Web Dashboard. It cycles endpoint install
4
- * targets by loading and persisting `settings.preferredToolMode` through
5
- * `/api/tool-mode`, while keeping URL hydration safe from late API responses.
6
- *
7
- * @functions
8
- * → useToolMode — loads, sets, and cycles the active endpoint target
9
- * @exports useToolMode
10
- */
11
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
12
- import { INSTALL_ENDPOINT_TOOL_MODES } from '../utils/m3.js'
13
-
14
- const DEFAULT_TOOL_MODE = 'opencode'
15
-
16
- function normalizeToolMode(mode) {
17
- return INSTALL_ENDPOINT_TOOL_MODES.includes(mode) ? mode : DEFAULT_TOOL_MODE
18
- }
19
-
20
- export function useToolMode({ onToast } = {}) {
21
- const [toolModeState, setToolModeState] = useState(DEFAULT_TOOL_MODE)
22
- const userSetRef = useRef(false)
23
-
24
- useEffect(() => {
25
- let cancelled = false
26
- async function loadToolMode() {
27
- try {
28
- const resp = await fetch('/api/tool-mode')
29
- const payload = await resp.json().catch(() => ({}))
30
- if (!cancelled && !userSetRef.current && resp.ok) {
31
- setToolModeState(normalizeToolMode(payload.mode))
32
- }
33
- } catch (err) {
34
- if (!cancelled) onToast?.(`Tool mode load failed: ${err.message}`, 'error')
35
- }
36
- }
37
- void loadToolMode()
38
- return () => { cancelled = true }
39
- }, [onToast])
40
-
41
- const persistToolMode = useCallback(async (mode) => {
42
- const normalized = normalizeToolMode(mode)
43
- userSetRef.current = true
44
- setToolModeState(normalized)
45
- try {
46
- const resp = await fetch('/api/tool-mode', {
47
- method: 'POST',
48
- headers: { 'Content-Type': 'application/json' },
49
- body: JSON.stringify({ mode: normalized }),
50
- })
51
- const payload = await resp.json().catch(() => ({}))
52
- if (!resp.ok) {
53
- throw new Error(payload.error || `HTTP ${resp.status}`)
54
- }
55
- setToolModeState(normalizeToolMode(payload.mode))
56
- return { ok: true, mode: normalizeToolMode(payload.mode) }
57
- } catch (err) {
58
- onToast?.(`Tool mode save failed: ${err.message}`, 'error')
59
- return { ok: false, error: err.message, mode: normalized }
60
- }
61
- }, [onToast])
62
-
63
- const cycleToolMode = useCallback(() => {
64
- const currentIndex = INSTALL_ENDPOINT_TOOL_MODES.indexOf(toolModeState)
65
- const next = INSTALL_ENDPOINT_TOOL_MODES[(currentIndex + 1) % INSTALL_ENDPOINT_TOOL_MODES.length] || DEFAULT_TOOL_MODE
66
- return persistToolMode(next)
67
- }, [persistToolMode, toolModeState])
68
-
69
- const tools = useMemo(() => [...INSTALL_ENDPOINT_TOOL_MODES], [])
70
-
71
- return {
72
- toolMode: toolModeState,
73
- tools,
74
- setToolMode: persistToolMode,
75
- cycleToolMode,
76
- }
77
- }
@@ -1,91 +0,0 @@
1
- /**
2
- * @file web/src/hooks/useUpdateChecker.js
3
- * @description React hook for the update chip + popover — M2 parity with TUI `Shift+U`.
4
- * 📖 Polls `/api/version` every 5 minutes (matches the TUI cadence) and exposes
5
- * 📖 `updateAvailable` (boolean) + `latestVersion` (string) for the header chip.
6
- * 📖 The chip's "Update now" button calls `/api/update/run` which spawns the
7
- * 📖 detected package manager in the background; the Web UI just surfaces a
8
- * 📖 toast and tells the user to restart the dashboard to apply the update.
9
- *
10
- * @functions
11
- * → useUpdateChecker({ onToast }) — { latestVersion, updateAvailable, runUpdate, checkNow, loading }
12
- */
13
- import { useCallback, useEffect, useState, useRef } from 'react'
14
-
15
- const POLL_INTERVAL_MS = 5 * 60_000
16
-
17
- // 📖 Lightweight semver compare: returns 1 if a > b, -1 if a < b, 0 if equal.
18
- function semverCompare(a, b) {
19
- if (!a || !b) return 0
20
- const ap = a.replace(/^v/, '').split('.').map(Number)
21
- const bp = b.replace(/^v/, '').split('.').map(Number)
22
- for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
23
- const av = ap[i] || 0
24
- const bv = bp[i] || 0
25
- if (bv !== av) return bv - av
26
- }
27
- return 0
28
- }
29
-
30
- export function useUpdateChecker({ onToast } = {}) {
31
- const [localVersion, setLocalVersion] = useState(null)
32
- const [latestVersion, setLatestVersion] = useState(null)
33
- const [lastReleaseDate, setLastReleaseDate] = useState(null)
34
- const [error, setError] = useState(null)
35
- const [loading, setLoading] = useState(false)
36
- const intervalRef = useRef(null)
37
-
38
- const checkNow = useCallback(async () => {
39
- setLoading(true)
40
- try {
41
- const resp = await fetch('/api/version')
42
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
43
- const data = await resp.json()
44
- setLocalVersion(data.local ?? null)
45
- setLatestVersion(data.latest ?? null)
46
- setLastReleaseDate(data.lastReleaseDate ?? null)
47
- setError(data.error ?? null)
48
- } catch (err) {
49
- setError(err.message || 'update check failed')
50
- } finally {
51
- setLoading(false)
52
- }
53
- }, [])
54
-
55
- // 📖 Initial check + 5-minute polling. We stop polling on unmount.
56
- useEffect(() => {
57
- checkNow()
58
- intervalRef.current = setInterval(checkNow, POLL_INTERVAL_MS)
59
- return () => {
60
- if (intervalRef.current) clearInterval(intervalRef.current)
61
- }
62
- }, [checkNow])
63
-
64
- // 📖 updateAvailable is true when latest > local. A null latest means the
65
- // 📖 npm registry is unreachable, so we don't surface a chip.
66
- const updateAvailable = Boolean(latestVersion && localVersion && semverCompare(latestVersion, localVersion) > 0)
67
-
68
- const runUpdate = useCallback(async () => {
69
- if (!updateAvailable) {
70
- onToast?.('No update available.', 'info')
71
- return
72
- }
73
- try {
74
- const resp = await fetch('/api/update/run', {
75
- method: 'POST',
76
- headers: { 'Content-Type': 'application/json' },
77
- body: JSON.stringify({ version: latestVersion }),
78
- })
79
- const data = await resp.json().catch(() => ({}))
80
- if (resp.ok && data?.started) {
81
- onToast?.(`Update to v${latestVersion} started — restart the dashboard to apply.`, 'success')
82
- } else {
83
- onToast?.(data?.error || data?.message || 'Update failed', 'error')
84
- }
85
- } catch (err) {
86
- onToast?.(err.message || 'Update request failed', 'error')
87
- }
88
- }, [updateAvailable, latestVersion, onToast])
89
-
90
- return { localVersion, latestVersion, lastReleaseDate, updateAvailable, checkNow, runUpdate, loading, error }
91
- }
@@ -1,170 +0,0 @@
1
- /**
2
- * @file web/src/hooks/useUrlState.js
3
- * @description URL deep-linking hook — the Web's answer to TUI CLI flags (--tier, --sort, etc.).
4
- * 📖 M1 = read-only hydration on mount.
5
- * 📖 M2 = adds write-back: every filter / sort / view / tool-mode / palette
6
- * 📖 change pushes to the URL via `history.replaceState` so the URL stays
7
- * 📖 in sync with the visible Web state and any URL is shareable.
8
- *
9
- * 📖 URL params (all optional, all shareable):
10
- * 📖 ?view=dashboard|settings|analytics|recommend|router|help|changelog
11
- * 📖 ?tier=S+|S|A+|A|A-|B+|B|C|all
12
- * 📖 ?status=up|down|pending|all
13
- * 📖 ?provider=<providerKey>|all
14
- * 📖 ?verdict=<verdict>|all
15
- * 📖 ?health=<health>|all
16
- * 📖 ?sort=<col>&dir=asc|desc
17
- * 📖 ?q=<searchText>
18
- * 📖 ?toolMode=<toolKey>
19
- * 📖 ?palette=open (when the palette should be open on load)
20
- *
21
- * @functions
22
- * → useUrlState({ currentView, setCurrentView, filterState, paletteOpen, setPaletteOpen })
23
- * → buildUrlParams(state) — pure helper exposed for tests
24
- */
25
- import { useEffect, useRef } from 'react'
26
- import { VALID_TIERS, VALID_STATUS, VALID_SORTS, VALID_VIEWS, VALID_DIRS, VALID_TOOL_MODES } from './urlState.constants.js'
27
-
28
- // 📖 Read the current URL params as a normalized object. Returns null on SSR
29
- // 📖 or when the URL is invalid.
30
- function parseUrlParams() {
31
- if (typeof window === 'undefined') return null
32
- const params = new URLSearchParams(window.location.search)
33
- const out = {}
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
-
45
- if (params.has('tier') && VALID_TIERS.has(params.get('tier'))) out.tier = params.get('tier')
46
- if (params.has('status') && VALID_STATUS.has(params.get('status'))) out.status = params.get('status')
47
- if (params.has('provider')) out.provider = params.get('provider')
48
- if (params.has('verdict')) out.verdict = params.get('verdict')
49
- if (params.has('health')) out.health = params.get('health')
50
- if (params.has('sort') && VALID_SORTS.has(params.get('sort'))) out.sort = params.get('sort')
51
- if (params.has('dir') && VALID_DIRS.has(params.get('dir'))) out.dir = params.get('dir')
52
- if (params.has('q')) out.q = params.get('q')
53
- if (params.has('toolMode') && VALID_TOOL_MODES.has(params.get('toolMode'))) out.toolMode = params.get('toolMode')
54
- if (params.has('recommend') && ['1', 'true', 'open'].includes(params.get('recommend'))) out.view = 'recommend'
55
- if (params.has('palette') && params.get('palette') === 'open') out.palette = 'open'
56
- return out
57
- }
58
-
59
- // 📖 Build a URLSearchParams object from the live state. Pure for testing.
60
- export function buildUrlParams(state) {
61
- const params = new URLSearchParams()
62
- if (!state) return params
63
- if (state.currentView && state.currentView !== 'dashboard') params.set('view', state.currentView)
64
- if (state.filterTier && state.filterTier !== 'all') params.set('tier', state.filterTier)
65
- if (state.filterStatus && state.filterStatus !== 'all') params.set('status', state.filterStatus)
66
- if (state.filterProvider && state.filterProvider !== 'all') params.set('provider', state.filterProvider)
67
- if (state.filterVerdict && state.filterVerdict !== 'all') params.set('verdict', state.filterVerdict)
68
- if (state.filterHealth && state.filterHealth !== 'all') params.set('health', state.filterHealth)
69
- if (state.sortColumn) {
70
- params.set('sort', state.sortColumn)
71
- if (state.sortDirection) params.set('dir', state.sortDirection)
72
- }
73
- if (state.searchQuery) params.set('q', state.searchQuery)
74
- if (state.toolMode) params.set('toolMode', state.toolMode)
75
- if (state.paletteOpen) params.set('palette', 'open')
76
- return params
77
- }
78
-
79
- // 📖 Debounced write-back helper. We don't want to push 5 history entries per
80
- // 📖 second when the user is typing in the search box.
81
- function writeUrl(state) {
82
- if (typeof window === 'undefined') return
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
-
89
- const search = params.toString()
90
- const path = view !== 'dashboard' ? `/${view}` : '/'
91
- const newUrl = search
92
- ? `${path}?${search}${window.location.hash}`
93
- : `${path}${window.location.hash}`
94
- // 📖 replaceState (not pushState) so back/forward don't fill up with
95
- // 📖 every keystroke. The current URL still updates visibly.
96
- window.history.replaceState({}, '', newUrl)
97
- }
98
-
99
- export function useUrlState({
100
- currentView, setCurrentView,
101
- filterState = null,
102
- paletteOpen = false, setPaletteOpen = () => {},
103
- toolMode = null, setToolMode = () => {},
104
- }) {
105
- // 📖 Track a debounce handle so we can coalesce rapid filter / sort changes.
106
- const writeTimerRef = useRef(null)
107
-
108
- // 📖 Hydrate from URL on mount, exactly once. We don't want to re-hydrate
109
- // 📖 on every render — the user's actions should drive the URL, not vice versa.
110
- const hydratedRef = useRef(false)
111
- useEffect(() => {
112
- if (hydratedRef.current) return
113
- hydratedRef.current = true
114
-
115
- const params = parseUrlParams()
116
- if (!params) return
117
-
118
- if (params.view && setCurrentView && params.view !== currentView) {
119
- setCurrentView(params.view)
120
- }
121
- if (params.tier && filterState?.setFilterTier) filterState.setFilterTier(params.tier)
122
- if (params.status && filterState?.setFilterStatus) filterState.setFilterStatus(params.status)
123
- if (params.provider && filterState?.setFilterProvider) filterState.setFilterProvider(params.provider)
124
- if (params.verdict && filterState?.setFilterVerdict) filterState.setFilterVerdict(params.verdict)
125
- if (params.health && filterState?.setFilterHealth) filterState.setFilterHealth(params.health)
126
- if (params.sort && filterState?.toggleSort) {
127
- // 📖 Re-trigger sort: pass the column to setSortColumn + setSortDirection
128
- // 📖 The hook exposes toggleSort which is a 3-state cycle; for hydration
129
- // 📖 we want a deterministic state, so we use the lower-level setters.
130
- if (filterState.setSortColumn) filterState.setSortColumn(params.sort)
131
- if (params.dir && filterState.setSortDirection) filterState.setSortDirection(params.dir)
132
- }
133
- if (params.q !== undefined && filterState?.setSearchQuery) filterState.setSearchQuery(params.q)
134
- if (params.toolMode && setToolMode) setToolMode(params.toolMode)
135
- if (params.palette === 'open' && setPaletteOpen) setPaletteOpen(true)
136
- // eslint-disable-next-line react-hooks/exhaustive-deps
137
- }, [])
138
-
139
- // 📖 Write-back: any change to currentView / filter / toolMode / palette
140
- // 📖 updates the URL. Debounced at 80ms so rapid filter typing doesn't
141
- // 📖 thrash the history stack.
142
- useEffect(() => {
143
- if (!hydratedRef.current) return
144
- if (writeTimerRef.current) clearTimeout(writeTimerRef.current)
145
- writeTimerRef.current = setTimeout(() => {
146
- writeUrl({
147
- currentView,
148
- filterTier: filterState?.filterTier,
149
- filterStatus: filterState?.filterStatus,
150
- filterProvider: filterState?.filterProvider,
151
- filterVerdict: filterState?.filterVerdict,
152
- filterHealth: filterState?.filterHealth,
153
- sortColumn: filterState?.sortColumn,
154
- sortDirection: filterState?.sortDirection,
155
- searchQuery: filterState?.searchQuery,
156
- toolMode,
157
- paletteOpen,
158
- })
159
- }, 80)
160
- return () => {
161
- if (writeTimerRef.current) clearTimeout(writeTimerRef.current)
162
- }
163
- }, [
164
- currentView, paletteOpen, toolMode,
165
- filterState?.filterTier, filterState?.filterStatus, filterState?.filterProvider,
166
- filterState?.filterVerdict, filterState?.filterHealth,
167
- filterState?.sortColumn, filterState?.sortDirection,
168
- filterState?.searchQuery,
169
- ])
170
- }
package/web/src/main.jsx DELETED
@@ -1,15 +0,0 @@
1
- /**
2
- * @file web/src/main.jsx
3
- * @description React application entry point — mounts the App component into the DOM.
4
- * 📖 Imports global CSS (design tokens, base styles, background effects), then renders <App />.
5
- */
6
- import { StrictMode } from 'react'
7
- import { createRoot } from 'react-dom/client'
8
- import App from './App.jsx'
9
- import './global.css'
10
-
11
- createRoot(document.getElementById('root')).render(
12
- <StrictMode>
13
- <App />
14
- </StrictMode>,
15
- )
@@ -1,15 +0,0 @@
1
- /**
2
- * @file web/src/utils/download.js
3
- * @description File download helper for export functionality.
4
- * → downloadFile
5
- */
6
-
7
- export function downloadFile(content, filename, type) {
8
- const blob = new Blob([content], { type })
9
- const url = URL.createObjectURL(blob)
10
- const a = document.createElement('a')
11
- a.href = url
12
- a.download = filename
13
- a.click()
14
- URL.revokeObjectURL(url)
15
- }
@@ -1,42 +0,0 @@
1
- /**
2
- * @file web/src/utils/format.js
3
- * @description Shared formatting utilities for the web dashboard.
4
- * → formatPing, formatAvg, formatCtx, maskKey, escapeHtml
5
- */
6
-
7
- export function formatPing(ms, code) {
8
- if (ms == null) return { text: '—', cls: 'pingNone' }
9
- if (code === '429') return { text: '429', cls: 'pingSlow' }
10
- if (code === '000') return { text: 'TIMEOUT', cls: 'pingSlow' }
11
- return { text: `${ms}ms`, cls: pingClass(ms) }
12
- }
13
-
14
- export function formatAvg(avg) {
15
- if (avg == null || avg === Infinity || avg > 99000) return { text: '—', cls: 'pingNone' }
16
- return { text: `${avg}ms`, cls: pingClass(avg) }
17
- }
18
-
19
- export function pingClass(ms) {
20
- if (ms == null || ms === Infinity) return 'pingNone'
21
- if (ms < 500) return 'pingFast'
22
- if (ms < 1500) return 'pingMedium'
23
- return 'pingSlow'
24
- }
25
-
26
- export function formatCtx(c) {
27
- if (!c || c === '—') return 0
28
- const s = c.toLowerCase()
29
- if (s.includes('m')) return parseFloat(s) * 1000
30
- if (s.includes('k')) return parseFloat(s)
31
- return 0
32
- }
33
-
34
- export function maskKey(key) {
35
- if (!key || key.length < 8) return '••••••••'
36
- return '••••••••' + key.slice(-4)
37
- }
38
-
39
- export function escapeHtml(str) {
40
- if (!str) return ''
41
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
42
- }
@@ -1,57 +0,0 @@
1
- /**
2
- * @file web/src/utils/m3.js
3
- * @description Small pure helpers for M3 Web parity tests. They keep payload
4
- * validation/formatting deterministic without importing React components.
5
- *
6
- * @functions
7
- * → recommendScoreShape — validates `/api/recommend` Top 3 payload shape
8
- * → toolInstallSummary — formats a tool install plan for display
9
- * @exports INSTALL_ENDPOINT_TOOL_MODES, recommendScoreShape, toolInstallSummary
10
- */
11
-
12
- export const INSTALL_ENDPOINT_TOOL_MODES = [
13
- 'opencode',
14
- 'pi',
15
- 'jcode',
16
- 'opencode-desktop',
17
- 'opencode-web',
18
- 'openclaw',
19
- 'crush',
20
- 'goose',
21
- 'aider',
22
- 'qwen',
23
- 'openhands',
24
- 'amp',
25
- 'forgecode',
26
- 'fcm_router',
27
- 'zcode',
28
- ]
29
-
30
- export function recommendScoreShape(payload) {
31
- const top3 = Array.isArray(payload?.top3) ? payload.top3 : []
32
- return top3.every((entry) => {
33
- const result = entry?.result
34
- return Boolean(result)
35
- && typeof result.providerKey === 'string'
36
- && typeof result.modelId === 'string'
37
- && typeof result.label === 'string'
38
- && typeof entry.score === 'number'
39
- && entry.score >= 0
40
- && entry.score <= 100
41
- && typeof entry.reason === 'string'
42
- && entry.reason.length > 0
43
- })
44
- }
45
-
46
- export function toolInstallSummary(plan) {
47
- if (!plan || typeof plan !== 'object') {
48
- return { supported: false, title: 'Unknown tool', command: null, docsUrl: null, note: null }
49
- }
50
- return {
51
- supported: plan.supported === true,
52
- title: plan.summary || plan.reason || `Install ${plan.mode || 'tool'}`,
53
- command: typeof plan.shellCommand === 'string' ? plan.shellCommand : null,
54
- docsUrl: typeof plan.docsUrl === 'string' ? plan.docsUrl : null,
55
- note: typeof plan.note === 'string' ? plan.note : null,
56
- }
57
- }
@@ -1,37 +0,0 @@
1
- /**
2
- * @file web/src/utils/ranks.js
3
- * @description Tier, verdict, and SWE ranking maps and helpers.
4
- * → tierRank, verdictRank, parseSwe, sweClass
5
- */
6
-
7
- const TIER_RANKS = { 'S+': 0, S: 1, 'A+': 2, A: 3, 'A-': 4, 'B+': 5, B: 6, C: 7 }
8
- const VERDICT_RANKS = {
9
- Perfect: 0, Normal: 1, Slow: 2, Spiky: 3,
10
- 'Very Slow': 4, Overloaded: 5, Unstable: 6,
11
- 'Not Active': 7, Pending: 8,
12
- }
13
-
14
- export function tierRank(tier) { return TIER_RANKS[tier] ?? 99 }
15
- export function verdictRank(verdict) { return VERDICT_RANKS[verdict] ?? 99 }
16
-
17
- export function parseSwe(s) {
18
- if (!s || s === '—') return 0
19
- return parseFloat(s.replace('%', '')) || 0
20
- }
21
-
22
- export function sweClass(swe) {
23
- const val = parseSwe(swe)
24
- if (val >= 65) return 'sweHigh'
25
- if (val >= 40) return 'sweMid'
26
- return 'sweLow'
27
- }
28
-
29
- export function verdictCls(verdict) {
30
- if (!verdict) return 'pending'
31
- const map = {
32
- perfect: 'perfect', normal: 'normal', slow: 'slow',
33
- spiky: 'spiky', veryslow: 'veryslow', overloaded: 'overloaded',
34
- unstable: 'unstable', notactive: 'notactive', pending: 'pending',
35
- }
36
- return map[verdict.toLowerCase().replace(/\s+/g, '')] || 'pending'
37
- }