free-coding-models 0.5.86 → 0.5.88

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 (165) hide show
  1. package/README.md +109 -888
  2. package/bin/free-coding-models.js +4 -1
  3. package/changelog/v0.5.87.md +48 -0
  4. package/changelog/v0.5.88.md +16 -0
  5. package/package.json +13 -5
  6. package/sources.js +4 -4
  7. package/src/core/cache.js +3 -1
  8. package/src/core/config.js +43 -5
  9. package/src/core/endpoint-installer.js +23 -12
  10. package/src/core/installed-models-manager.js +43 -20
  11. package/src/core/kilo-config.js +51 -5
  12. package/src/core/kilo.js +5 -3
  13. package/src/core/legacy-proxy-cleanup.js +22 -5
  14. package/src/core/model-merger.js +4 -0
  15. package/src/core/models-drift.js +9 -0
  16. package/src/core/opencode-config.js +57 -5
  17. package/src/core/opencode.js +67 -41
  18. package/src/core/playground.js +7 -0
  19. package/src/core/probe-cache.js +35 -4
  20. package/src/core/provider-key-tester.js +10 -2
  21. package/src/core/provider-metadata.js +10 -0
  22. package/src/core/provider-quota-fetchers.js +14 -7
  23. package/src/core/router-daemon.js +208 -26
  24. package/src/core/runtime-telemetry.js +27 -1
  25. package/src/core/security.js +170 -24
  26. package/src/core/shared-helpers.js +12 -1
  27. package/src/core/shell-env.js +3 -1
  28. package/src/core/telemetry.js +18 -5
  29. package/src/core/tool-launchers.js +9 -6
  30. package/src/core/updater.js +28 -2
  31. package/src/core/utils.js +173 -71
  32. package/src/tui/app.js +38 -14
  33. package/src/tui/command-palette.js +22 -2
  34. package/src/tui/key-handler.js +123 -179
  35. package/src/tui/overlays.js +21 -71
  36. package/src/tui/render-helpers.js +62 -4
  37. package/src/tui/render-table.js +34 -3
  38. package/src/tui/theme.js +36 -5
  39. package/src/tui/tui-filters.js +28 -8
  40. package/src/tui/tui-state.js +22 -6
  41. package/web/dist/assets/index-CAzFIt8P.css +1 -0
  42. package/web/dist/assets/index-DFg1h0Nd.js +44 -0
  43. package/web/dist/index.html +5 -9
  44. package/web/index.html +3 -7
  45. package/web/server.js +97 -23
  46. package/web/vite.config.js +8 -0
  47. package/web/app.legacy.js +0 -900
  48. package/web/dist/assets/index-C5-Ywvv3.css +0 -1
  49. package/web/dist/assets/index-Dm-Xfby9.js +0 -40
  50. package/web/public/favicon.ico +0 -0
  51. package/web/public/favicons/apple-touch-icon.png +0 -0
  52. package/web/public/favicons/browserconfig.xml +0 -12
  53. package/web/public/favicons/favicon-16x16.png +0 -0
  54. package/web/public/favicons/favicon-192x192.png +0 -0
  55. package/web/public/favicons/favicon-32x32.png +0 -0
  56. package/web/public/favicons/favicon-48x48.png +0 -0
  57. package/web/public/favicons/favicon-512x512.png +0 -0
  58. package/web/public/favicons/favicon-96x96.png +0 -0
  59. package/web/public/favicons/favicon.ico +0 -0
  60. package/web/public/favicons/mstile-150x150.png +0 -0
  61. package/web/public/favicons/mstile-310x150.png +0 -0
  62. package/web/public/favicons/mstile-310x310.png +0 -0
  63. package/web/public/favicons/mstile-512x512.png +0 -0
  64. package/web/public/favicons/mstile-70x70.png +0 -0
  65. package/web/public/favicons/site.webmanifest +0 -25
  66. package/web/src/App.jsx +0 -521
  67. package/web/src/components/analytics/AnalyticsView.jsx +0 -147
  68. package/web/src/components/analytics/AnalyticsView.module.css +0 -190
  69. package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
  70. package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
  71. package/web/src/components/atoms/AILatencyCell.jsx +0 -38
  72. package/web/src/components/atoms/AILatencyCell.module.css +0 -51
  73. package/web/src/components/atoms/HealthCell.jsx +0 -143
  74. package/web/src/components/atoms/HealthCell.module.css +0 -60
  75. package/web/src/components/atoms/LastPingCell.jsx +0 -35
  76. package/web/src/components/atoms/LastPingCell.module.css +0 -45
  77. package/web/src/components/atoms/MoodCell.jsx +0 -25
  78. package/web/src/components/atoms/MoodCell.module.css +0 -6
  79. package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
  80. package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
  81. package/web/src/components/atoms/ProviderLogo.jsx +0 -187
  82. package/web/src/components/atoms/ProviderLogo.module.css +0 -103
  83. package/web/src/components/atoms/RankCell.jsx +0 -9
  84. package/web/src/components/atoms/RankCell.module.css +0 -9
  85. package/web/src/components/atoms/Sparkline.jsx +0 -63
  86. package/web/src/components/atoms/StabilityCell.jsx +0 -18
  87. package/web/src/components/atoms/StabilityCell.module.css +0 -13
  88. package/web/src/components/atoms/StatusDot.jsx +0 -13
  89. package/web/src/components/atoms/StatusDot.module.css +0 -29
  90. package/web/src/components/atoms/TPSCell.jsx +0 -36
  91. package/web/src/components/atoms/TPSCell.module.css +0 -44
  92. package/web/src/components/atoms/TierBadge.jsx +0 -10
  93. package/web/src/components/atoms/TierBadge.module.css +0 -29
  94. package/web/src/components/atoms/Toast.jsx +0 -25
  95. package/web/src/components/atoms/Toast.module.css +0 -41
  96. package/web/src/components/atoms/ToastContainer.jsx +0 -16
  97. package/web/src/components/atoms/ToastContainer.module.css +0 -10
  98. package/web/src/components/atoms/VerdictBadge.jsx +0 -36
  99. package/web/src/components/atoms/VerdictBadge.module.css +0 -40
  100. package/web/src/components/changelog/ChangelogView.jsx +0 -135
  101. package/web/src/components/changelog/ChangelogView.module.css +0 -159
  102. package/web/src/components/dashboard/DetailPanel.jsx +0 -217
  103. package/web/src/components/dashboard/DetailPanel.module.css +0 -260
  104. package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
  105. package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
  106. package/web/src/components/dashboard/ExportModal.jsx +0 -84
  107. package/web/src/components/dashboard/ExportModal.module.css +0 -99
  108. package/web/src/components/dashboard/FilterBar.jsx +0 -338
  109. package/web/src/components/dashboard/FilterBar.module.css +0 -342
  110. package/web/src/components/dashboard/ModelTable.jsx +0 -672
  111. package/web/src/components/dashboard/ModelTable.module.css +0 -446
  112. package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
  113. package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
  114. package/web/src/components/help/HelpView.jsx +0 -201
  115. package/web/src/components/help/HelpView.module.css +0 -156
  116. package/web/src/components/install/InstallEndpointsView.jsx +0 -309
  117. package/web/src/components/install/InstallEndpointsView.module.css +0 -427
  118. package/web/src/components/installed/InstalledModelsView.jsx +0 -89
  119. package/web/src/components/installed/InstalledModelsView.module.css +0 -200
  120. package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
  121. package/web/src/components/launch/LaunchButton.jsx +0 -30
  122. package/web/src/components/launch/LaunchButton.module.css +0 -35
  123. package/web/src/components/launch/LaunchModal.module.css +0 -125
  124. package/web/src/components/layout/Footer.jsx +0 -19
  125. package/web/src/components/layout/Footer.module.css +0 -10
  126. package/web/src/components/layout/Header.jsx +0 -265
  127. package/web/src/components/layout/Header.module.css +0 -357
  128. package/web/src/components/palette/CommandPalette.jsx +0 -301
  129. package/web/src/components/palette/CommandPalette.module.css +0 -121
  130. package/web/src/components/playground/PlaygroundChat.jsx +0 -527
  131. package/web/src/components/playground/PlaygroundChat.module.css +0 -382
  132. package/web/src/components/playground/PlaygroundView.jsx +0 -371
  133. package/web/src/components/playground/PlaygroundView.module.css +0 -504
  134. package/web/src/components/recommend/RecommendView.jsx +0 -121
  135. package/web/src/components/recommend/RecommendView.module.css +0 -57
  136. package/web/src/components/router/RouterView.jsx +0 -1212
  137. package/web/src/components/router/RouterView.module.css +0 -1289
  138. package/web/src/components/settings/SettingsView.jsx +0 -555
  139. package/web/src/components/settings/SettingsView.module.css +0 -550
  140. package/web/src/components/tools/ToolPicker.jsx +0 -86
  141. package/web/src/components/tools/ToolPicker.module.css +0 -97
  142. package/web/src/components/update/UpdateChip.jsx +0 -104
  143. package/web/src/components/update/UpdateChip.module.css +0 -146
  144. package/web/src/global.css +0 -298
  145. package/web/src/hooks/urlState.constants.js +0 -28
  146. package/web/src/hooks/useChangelog.js +0 -51
  147. package/web/src/hooks/useColumnSizing.js +0 -211
  148. package/web/src/hooks/useFavorites.js +0 -150
  149. package/web/src/hooks/useFilter.js +0 -310
  150. package/web/src/hooks/useInstalledModels.js +0 -42
  151. package/web/src/hooks/useRecommend.js +0 -67
  152. package/web/src/hooks/useRouterDashboard.js +0 -133
  153. package/web/src/hooks/useSSE.js +0 -73
  154. package/web/src/hooks/useSocket.js +0 -203
  155. package/web/src/hooks/useTheme.js +0 -83
  156. package/web/src/hooks/useTokenUsage.js +0 -103
  157. package/web/src/hooks/useToolMode.js +0 -77
  158. package/web/src/hooks/useUpdateChecker.js +0 -91
  159. package/web/src/hooks/useUrlState.js +0 -170
  160. package/web/src/main.jsx +0 -15
  161. package/web/src/utils/download.js +0 -15
  162. package/web/src/utils/format.js +0 -42
  163. package/web/src/utils/m3.js +0 -57
  164. package/web/src/utils/ranks.js +0 -37
  165. 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
- }