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.
- package/README.md +110 -881
- package/bin/free-coding-models.js +4 -1
- package/changelog/v0.5.87.md +48 -0
- package/package.json +8 -5
- package/sources.js +4 -4
- package/src/core/cache.js +3 -1
- package/src/core/config.js +43 -5
- package/src/core/endpoint-installer.js +23 -12
- package/src/core/installed-models-manager.js +43 -20
- package/src/core/kilo-config.js +51 -5
- package/src/core/kilo.js +5 -3
- package/src/core/legacy-proxy-cleanup.js +22 -5
- package/src/core/model-merger.js +4 -0
- package/src/core/models-drift.js +9 -0
- package/src/core/opencode-config.js +57 -5
- package/src/core/opencode.js +67 -41
- package/src/core/playground.js +7 -0
- package/src/core/probe-cache.js +35 -4
- package/src/core/provider-key-tester.js +10 -2
- package/src/core/provider-metadata.js +10 -0
- package/src/core/provider-quota-fetchers.js +14 -7
- package/src/core/router-daemon.js +208 -26
- package/src/core/runtime-telemetry.js +27 -1
- package/src/core/shared-helpers.js +12 -1
- package/src/core/shell-env.js +3 -1
- package/src/core/telemetry.js +18 -5
- package/src/core/tool-launchers.js +9 -6
- package/src/core/updater.js +28 -2
- package/src/core/utils.js +76 -71
- package/src/tui/app.js +38 -14
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +21 -71
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +34 -3
- package/src/tui/theme.js +36 -5
- package/src/tui/tui-filters.js +28 -8
- package/src/tui/tui-state.js +22 -6
- package/web/dist/assets/index-BpTpPS0g.js +44 -0
- package/web/dist/assets/index-CAzFIt8P.css +1 -0
- package/web/dist/index.html +5 -9
- package/web/index.html +3 -7
- package/web/server.js +97 -23
- package/web/vite.config.js +8 -0
- package/web/app.legacy.js +0 -900
- package/web/dist/assets/index-C5-Ywvv3.css +0 -1
- package/web/dist/assets/index-Dm-Xfby9.js +0 -40
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/browserconfig.xml +0 -12
- package/web/public/favicons/favicon-16x16.png +0 -0
- package/web/public/favicons/favicon-192x192.png +0 -0
- package/web/public/favicons/favicon-32x32.png +0 -0
- package/web/public/favicons/favicon-48x48.png +0 -0
- package/web/public/favicons/favicon-512x512.png +0 -0
- package/web/public/favicons/favicon-96x96.png +0 -0
- package/web/public/favicons/favicon.ico +0 -0
- package/web/public/favicons/mstile-150x150.png +0 -0
- package/web/public/favicons/mstile-310x150.png +0 -0
- package/web/public/favicons/mstile-310x310.png +0 -0
- package/web/public/favicons/mstile-512x512.png +0 -0
- package/web/public/favicons/mstile-70x70.png +0 -0
- package/web/public/favicons/site.webmanifest +0 -25
- package/web/src/App.jsx +0 -521
- package/web/src/components/analytics/AnalyticsView.jsx +0 -147
- package/web/src/components/analytics/AnalyticsView.module.css +0 -190
- package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
- package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
- package/web/src/components/atoms/AILatencyCell.jsx +0 -38
- package/web/src/components/atoms/AILatencyCell.module.css +0 -51
- package/web/src/components/atoms/HealthCell.jsx +0 -143
- package/web/src/components/atoms/HealthCell.module.css +0 -60
- package/web/src/components/atoms/LastPingCell.jsx +0 -35
- package/web/src/components/atoms/LastPingCell.module.css +0 -45
- package/web/src/components/atoms/MoodCell.jsx +0 -25
- package/web/src/components/atoms/MoodCell.module.css +0 -6
- package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
- package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
- package/web/src/components/atoms/ProviderLogo.jsx +0 -187
- package/web/src/components/atoms/ProviderLogo.module.css +0 -103
- package/web/src/components/atoms/RankCell.jsx +0 -9
- package/web/src/components/atoms/RankCell.module.css +0 -9
- package/web/src/components/atoms/Sparkline.jsx +0 -63
- package/web/src/components/atoms/StabilityCell.jsx +0 -18
- package/web/src/components/atoms/StabilityCell.module.css +0 -13
- package/web/src/components/atoms/StatusDot.jsx +0 -13
- package/web/src/components/atoms/StatusDot.module.css +0 -29
- package/web/src/components/atoms/TPSCell.jsx +0 -36
- package/web/src/components/atoms/TPSCell.module.css +0 -44
- package/web/src/components/atoms/TierBadge.jsx +0 -10
- package/web/src/components/atoms/TierBadge.module.css +0 -29
- package/web/src/components/atoms/Toast.jsx +0 -25
- package/web/src/components/atoms/Toast.module.css +0 -41
- package/web/src/components/atoms/ToastContainer.jsx +0 -16
- package/web/src/components/atoms/ToastContainer.module.css +0 -10
- package/web/src/components/atoms/VerdictBadge.jsx +0 -36
- package/web/src/components/atoms/VerdictBadge.module.css +0 -40
- package/web/src/components/changelog/ChangelogView.jsx +0 -135
- package/web/src/components/changelog/ChangelogView.module.css +0 -159
- package/web/src/components/dashboard/DetailPanel.jsx +0 -217
- package/web/src/components/dashboard/DetailPanel.module.css +0 -260
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
- package/web/src/components/dashboard/ExportModal.jsx +0 -84
- package/web/src/components/dashboard/ExportModal.module.css +0 -99
- package/web/src/components/dashboard/FilterBar.jsx +0 -338
- package/web/src/components/dashboard/FilterBar.module.css +0 -342
- package/web/src/components/dashboard/ModelTable.jsx +0 -672
- package/web/src/components/dashboard/ModelTable.module.css +0 -446
- package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
- package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
- package/web/src/components/help/HelpView.jsx +0 -201
- package/web/src/components/help/HelpView.module.css +0 -156
- package/web/src/components/install/InstallEndpointsView.jsx +0 -309
- package/web/src/components/install/InstallEndpointsView.module.css +0 -427
- package/web/src/components/installed/InstalledModelsView.jsx +0 -89
- package/web/src/components/installed/InstalledModelsView.module.css +0 -200
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
- package/web/src/components/launch/LaunchButton.jsx +0 -30
- package/web/src/components/launch/LaunchButton.module.css +0 -35
- package/web/src/components/launch/LaunchModal.module.css +0 -125
- package/web/src/components/layout/Footer.jsx +0 -19
- package/web/src/components/layout/Footer.module.css +0 -10
- package/web/src/components/layout/Header.jsx +0 -265
- package/web/src/components/layout/Header.module.css +0 -357
- package/web/src/components/palette/CommandPalette.jsx +0 -301
- package/web/src/components/palette/CommandPalette.module.css +0 -121
- package/web/src/components/playground/PlaygroundChat.jsx +0 -527
- package/web/src/components/playground/PlaygroundChat.module.css +0 -382
- package/web/src/components/playground/PlaygroundView.jsx +0 -371
- package/web/src/components/playground/PlaygroundView.module.css +0 -504
- package/web/src/components/recommend/RecommendView.jsx +0 -121
- package/web/src/components/recommend/RecommendView.module.css +0 -57
- package/web/src/components/router/RouterView.jsx +0 -1212
- package/web/src/components/router/RouterView.module.css +0 -1289
- package/web/src/components/settings/SettingsView.jsx +0 -555
- package/web/src/components/settings/SettingsView.module.css +0 -550
- package/web/src/components/tools/ToolPicker.jsx +0 -86
- package/web/src/components/tools/ToolPicker.module.css +0 -97
- package/web/src/components/update/UpdateChip.jsx +0 -104
- package/web/src/components/update/UpdateChip.module.css +0 -146
- package/web/src/global.css +0 -298
- package/web/src/hooks/urlState.constants.js +0 -28
- package/web/src/hooks/useChangelog.js +0 -51
- package/web/src/hooks/useColumnSizing.js +0 -211
- package/web/src/hooks/useFavorites.js +0 -150
- package/web/src/hooks/useFilter.js +0 -310
- package/web/src/hooks/useInstalledModels.js +0 -42
- package/web/src/hooks/useRecommend.js +0 -67
- package/web/src/hooks/useRouterDashboard.js +0 -133
- package/web/src/hooks/useSSE.js +0 -73
- package/web/src/hooks/useSocket.js +0 -203
- package/web/src/hooks/useTheme.js +0 -83
- package/web/src/hooks/useTokenUsage.js +0 -103
- package/web/src/hooks/useToolMode.js +0 -77
- package/web/src/hooks/useUpdateChecker.js +0 -91
- package/web/src/hooks/useUrlState.js +0 -170
- package/web/src/main.jsx +0 -15
- package/web/src/utils/download.js +0 -15
- package/web/src/utils/format.js +0 -42
- package/web/src/utils/m3.js +0 -57
- package/web/src/utils/ranks.js +0 -37
- package/web/styles.legacy.css +0 -963
package/src/tui/key-handler.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* tool launch actions. It also keeps the live key bindings aligned with the
|
|
9
9
|
* highlighted letters shown in the table headers.
|
|
10
10
|
*
|
|
11
|
-
* 📖 Key I opens the
|
|
11
|
+
* 📖 Key I opens the Help overlay. The changelog lives in Settings
|
|
12
|
+
* 📖 ("View Changelog" row) and the command palette ("Changelog" entry).
|
|
12
13
|
*
|
|
13
14
|
* It also owns the "test key" model selection used by the Settings overlay,
|
|
14
15
|
* delegated to the shared `provider-key-tester.js` module for consistency
|
|
@@ -20,7 +21,6 @@
|
|
|
20
21
|
* @exports { buildProviderModelsUrl, parseProviderModelIds, listProviderTestModels, classifyProviderTestOutcome, buildProviderTestDetail, createKeyHandler }
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
|
-
import { loadChangelog } from '../core/changelog-loader.js'
|
|
24
24
|
import { getToolMeta, isModelCompatibleWithTool, getCompatibleTools, findSimilarCompatibleModels } from '../core/tool-metadata.js'
|
|
25
25
|
import { loadConfig, saveConfig, replaceConfigContents, getApiKey } from '../core/config.js'
|
|
26
26
|
import { sources } from '../../sources.js'
|
|
@@ -31,7 +31,8 @@ import { cleanupLegacyProxyArtifacts } from '../core/legacy-proxy-cleanup.js'
|
|
|
31
31
|
import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
|
|
32
32
|
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
33
33
|
import { syncShellEnv, ensureShellRcSource, removeShellEnv } from '../core/shell-env.js'
|
|
34
|
-
import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntries } from './command-palette.js'
|
|
34
|
+
import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntries, clampPaletteCursor } from './command-palette.js'
|
|
35
|
+
import { runWithConcurrency, loadChangelogCached } from './render-helpers.js'
|
|
35
36
|
import { WIDTH_WARNING_MIN_COLS, VERDICT_CYCLE, HEALTH_CYCLE } from '../core/constants.js'
|
|
36
37
|
import { scanAllToolConfigs, softDeleteModel } from '../core/installed-models-manager.js'
|
|
37
38
|
import { startExternalTool } from '../core/tool-launchers.js'
|
|
@@ -109,6 +110,32 @@ function spawnDaemonCommand(state, args) {
|
|
|
109
110
|
})
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
// 📖 persistTableViewSettings: single source of truth for persisting table-view
|
|
114
|
+
// 📖 preferences (sort column/direction, tier/origin filter). Keyboard and mouse
|
|
115
|
+
// 📖 handlers both call this so a mouse sort persists exactly like a keyboard
|
|
116
|
+
// 📖 sort: via settings.sortAsc + saveConfig. The old mouse-only copy wrote a
|
|
117
|
+
// 📖 settings.sortDirection field nothing reads and never called saveConfig, so
|
|
118
|
+
// 📖 mouse sort changes were silently lost on restart. ORIGIN_CYCLE is optional:
|
|
119
|
+
// 📖 the mouse handler never changes the origin filter (and doesn't receive the
|
|
120
|
+
// 📖 cycle), so it just leaves that field untouched.
|
|
121
|
+
function persistTableViewSettings(state, { TIER_CYCLE = null, ORIGIN_CYCLE = null, saveConfig }) {
|
|
122
|
+
if (!state.config) return
|
|
123
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
124
|
+
if (TIER_CYCLE) state.config.settings.tierFilter = TIER_CYCLE[state.tierFilterMode]
|
|
125
|
+
if (ORIGIN_CYCLE) state.config.settings.originFilter = ORIGIN_CYCLE[state.originFilterMode] ?? null
|
|
126
|
+
state.config.settings.sortColumn = state.sortColumn
|
|
127
|
+
state.config.settings.sortAsc = state.sortDirection === 'asc'
|
|
128
|
+
saveConfig(state.config)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 📖 Safe "scroll to end" sentinel for overlay End keys. The overlay renderers
|
|
132
|
+
// 📖 clamp the offset to the real content length on the next frame
|
|
133
|
+
// 📖 (clampOverlayOffset via sliceOverlayLines), so the value only needs to be
|
|
134
|
+
// 📖 far past any plausible content. This replaces the old
|
|
135
|
+
// 📖 Number.MAX_SAFE_INTEGER assignments that leaked into downstream scroll
|
|
136
|
+
// 📖 arithmetic (pageStep +/- offsets) before the next render clamped them.
|
|
137
|
+
const OVERLAY_SCROLL_END_SENTINEL = 100_000
|
|
138
|
+
|
|
112
139
|
export function createKeyHandler(ctx) {
|
|
113
140
|
const {
|
|
114
141
|
state,
|
|
@@ -165,9 +192,7 @@ export function createKeyHandler(ctx) {
|
|
|
165
192
|
noteUserActivity,
|
|
166
193
|
intervalToPingMode,
|
|
167
194
|
PING_MODE_CYCLE,
|
|
168
|
-
themeRowIdx,
|
|
169
195
|
setResults,
|
|
170
|
-
readline,
|
|
171
196
|
} = ctx
|
|
172
197
|
|
|
173
198
|
let userSelected = null
|
|
@@ -177,6 +202,27 @@ export function createKeyHandler(ctx) {
|
|
|
177
202
|
// 📖 the probe counters and skip their reset instead of clobbering live values.
|
|
178
203
|
let probeRunSeq = 0
|
|
179
204
|
|
|
205
|
+
// 📖 Tracked error-message timers (P2 fix): the setTimeout handles that clear
|
|
206
|
+
// 📖 installedModelsErrorMsg / settingsErrorMsg used to be anonymous, so
|
|
207
|
+
// 📖 closing + reopening an overlay within 3s let an OLD timer wipe a FRESH
|
|
208
|
+
// 📖 error. Handles now live on state and are cleared whenever the overlay
|
|
209
|
+
// 📖 opens/closes or a new error is scheduled.
|
|
210
|
+
function clearErrorMsgTimer(field) {
|
|
211
|
+
const timerKey = `${field}ClearTimer`
|
|
212
|
+
if (state[timerKey]) {
|
|
213
|
+
clearTimeout(state[timerKey])
|
|
214
|
+
state[timerKey] = null
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function scheduleErrorMsgClear(field, ms) {
|
|
219
|
+
clearErrorMsgTimer(field)
|
|
220
|
+
state[`${field}ClearTimer`] = setTimeout(() => {
|
|
221
|
+
state[field] = null
|
|
222
|
+
state[`${field}ClearTimer`] = null
|
|
223
|
+
}, ms)
|
|
224
|
+
}
|
|
225
|
+
|
|
180
226
|
function resetToolInstallPrompt() {
|
|
181
227
|
state.toolInstallPromptOpen = false
|
|
182
228
|
state.toolInstallPromptCursor = 0
|
|
@@ -680,13 +726,9 @@ export function createKeyHandler(ctx) {
|
|
|
680
726
|
}
|
|
681
727
|
|
|
682
728
|
// 📖 Persist current table-view preferences so sort/filter state survives restarts.
|
|
729
|
+
// 📖 Delegates to the module-level helper shared with the mouse handler.
|
|
683
730
|
function persistUiSettings() {
|
|
684
|
-
|
|
685
|
-
state.config.settings.tierFilter = TIER_CYCLE[state.tierFilterMode]
|
|
686
|
-
state.config.settings.originFilter = ORIGIN_CYCLE[state.originFilterMode] ?? null
|
|
687
|
-
state.config.settings.sortColumn = state.sortColumn
|
|
688
|
-
state.config.settings.sortAsc = state.sortDirection === 'asc'
|
|
689
|
-
saveConfig(state.config)
|
|
731
|
+
persistTableViewSettings(state, { TIER_CYCLE, ORIGIN_CYCLE, saveConfig })
|
|
690
732
|
}
|
|
691
733
|
|
|
692
734
|
// 📖 Shared table refresh helper so command-palette and hotkeys keep identical behavior.
|
|
@@ -731,6 +773,8 @@ export function createKeyHandler(ctx) {
|
|
|
731
773
|
state.settingsAddKeyMode = false
|
|
732
774
|
state.settingsEditBuffer = ''
|
|
733
775
|
state.settingsScrollOffset = 0
|
|
776
|
+
// 📖 Fresh overlay: drop any stale auto-clear timer from a previous visit.
|
|
777
|
+
clearErrorMsgTimer('settingsErrorMsg')
|
|
734
778
|
|
|
735
779
|
// 📖 Auto-test all configured API keys in parallel on Settings open.
|
|
736
780
|
// 📖 Each provider with a saved key fires a parallel auth probe batch immediately.
|
|
@@ -785,6 +829,8 @@ export function createKeyHandler(ctx) {
|
|
|
785
829
|
state.installedModelsOpen = true
|
|
786
830
|
state.installedModelsCursor = 0
|
|
787
831
|
state.installedModelsScrollOffset = 0
|
|
832
|
+
// 📖 Clear any stale auto-clear timer so it cannot wipe the fresh status.
|
|
833
|
+
clearErrorMsgTimer('installedModelsErrorMsg')
|
|
788
834
|
state.installedModelsErrorMsg = 'Scanning...'
|
|
789
835
|
|
|
790
836
|
try {
|
|
@@ -955,23 +1001,9 @@ export function createKeyHandler(ctx) {
|
|
|
955
1001
|
}
|
|
956
1002
|
}
|
|
957
1003
|
|
|
958
|
-
// 📖 runWithConcurrency
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
let nextIndex = 0
|
|
962
|
-
const workers = new Array(maxConcurrent).fill(null).map(async () => {
|
|
963
|
-
while (true) {
|
|
964
|
-
const index = nextIndex++
|
|
965
|
-
if (index >= tasks.length) break
|
|
966
|
-
try {
|
|
967
|
-
results[index] = await tasks[index]()
|
|
968
|
-
} catch (err) {
|
|
969
|
-
results[index] = { error: err }
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
})
|
|
973
|
-
return Promise.all(workers).then(() => results)
|
|
974
|
-
}
|
|
1004
|
+
// 📖 runWithConcurrency now lives in render-helpers.js (shared with app.js's
|
|
1005
|
+
// 📖 startup ping burst, which used to fire unbounded Promise.all). Imported
|
|
1006
|
+
// 📖 at the top of this file.
|
|
975
1007
|
|
|
976
1008
|
// 📖 setActionNotice: Surface a non-fatal error/info message as a footer chip.
|
|
977
1009
|
// 📖 Part of the issue #168 "kicked out" fix: palette commands and the 404 probe
|
|
@@ -1260,6 +1292,8 @@ export function createKeyHandler(ctx) {
|
|
|
1260
1292
|
|| state.routerDashboardOpen
|
|
1261
1293
|
|| state.playgroundOpen
|
|
1262
1294
|
|| state.recommendOpen
|
|
1295
|
+
|| state.tokenUsageOpen
|
|
1296
|
+
|| state.runtimeReportOpen
|
|
1263
1297
|
|
|
1264
1298
|
|| state.helpVisible
|
|
1265
1299
|
|| state.changelogOpen
|
|
@@ -1325,9 +1359,9 @@ export function createKeyHandler(ctx) {
|
|
|
1325
1359
|
})
|
|
1326
1360
|
}
|
|
1327
1361
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1362
|
+
// 📖 Clamp against the RENDERED slice (COMMAND_PALETTE_MAX_RESULTS rows),
|
|
1363
|
+
// 📖 not the full list - otherwise the cursor could sit on an invisible row.
|
|
1364
|
+
state.commandPaletteCursor = clampPaletteCursor(state.commandPaletteCursor, state.commandPaletteResults.length)
|
|
1331
1365
|
}
|
|
1332
1366
|
|
|
1333
1367
|
function openCommandPalette() {
|
|
@@ -1530,8 +1564,6 @@ export function createKeyHandler(ctx) {
|
|
|
1530
1564
|
|
|
1531
1565
|
// 📖 Command palette captures the keyboard while active.
|
|
1532
1566
|
if (state.commandPaletteOpen) {
|
|
1533
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1534
|
-
|
|
1535
1567
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 10)
|
|
1536
1568
|
const selected = state.commandPaletteResults[state.commandPaletteCursor]
|
|
1537
1569
|
|
|
@@ -1630,7 +1662,6 @@ export function createKeyHandler(ctx) {
|
|
|
1630
1662
|
// 📖 Router Dashboard: ↑↓ navigate favorites list, Ctrl+↑↓ reorder,
|
|
1631
1663
|
// 📖 I cycles health check speed, C clears log, Esc goes back.
|
|
1632
1664
|
if (state.routerDashboardOpen) {
|
|
1633
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1634
1665
|
const favorites = Array.isArray(state.config?.favorites) ? state.config.favorites : []
|
|
1635
1666
|
// 📖 maxCursor accounts for the favorites list + 2 buttons (Start/Stop Daemon and Install Endpoint)
|
|
1636
1667
|
const maxCursor = Math.max(0, favorites.length + 1)
|
|
@@ -1753,8 +1784,6 @@ export function createKeyHandler(ctx) {
|
|
|
1753
1784
|
|
|
1754
1785
|
// 📖 Install Endpoints overlay: provider → tool → connection → scope → optional model subset.
|
|
1755
1786
|
if (state.installEndpointsOpen) {
|
|
1756
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1757
|
-
|
|
1758
1787
|
const providerChoices = getConfiguredInstallableProviders(state.config)
|
|
1759
1788
|
const toolChoices = getInstallTargetModes().filter(t => !(state.installEndpointsProviderKey === 'fcm_router' && t === 'fcm_router'))
|
|
1760
1789
|
const modelChoices = state.installEndpointsProviderKey
|
|
@@ -1961,8 +1990,6 @@ export function createKeyHandler(ctx) {
|
|
|
1961
1990
|
}
|
|
1962
1991
|
|
|
1963
1992
|
if (state.toolInstallPromptOpen) {
|
|
1964
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1965
|
-
|
|
1966
1993
|
const installPlan = state.toolInstallPromptPlan || getToolInstallPlan(state.toolInstallPromptMode)
|
|
1967
1994
|
const installSupported = Boolean(installPlan?.supported)
|
|
1968
1995
|
|
|
@@ -2000,8 +2027,6 @@ export function createKeyHandler(ctx) {
|
|
|
2000
2027
|
|
|
2001
2028
|
// ─── Installed Models overlay keyboard handling ───────────────────────────
|
|
2002
2029
|
if (state.installedModelsOpen) {
|
|
2003
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
2004
|
-
|
|
2005
2030
|
const scanResults = state.installedModelsData || []
|
|
2006
2031
|
let maxIndex = 0
|
|
2007
2032
|
for (const toolResult of scanResults) {
|
|
@@ -2040,6 +2065,7 @@ export function createKeyHandler(ctx) {
|
|
|
2040
2065
|
if (key.name === 'escape') {
|
|
2041
2066
|
state.installedModelsOpen = false
|
|
2042
2067
|
state.installedModelsCursor = 0
|
|
2068
|
+
clearErrorMsgTimer('installedModelsErrorMsg')
|
|
2043
2069
|
return
|
|
2044
2070
|
}
|
|
2045
2071
|
|
|
@@ -2079,12 +2105,12 @@ export function createKeyHandler(ctx) {
|
|
|
2079
2105
|
openInstalledModelsOverlay()
|
|
2080
2106
|
} else {
|
|
2081
2107
|
state.installedModelsErrorMsg = `Failed to disable: ${result.error}`
|
|
2082
|
-
|
|
2108
|
+
scheduleErrorMsgClear('installedModelsErrorMsg', 3000)
|
|
2083
2109
|
}
|
|
2084
2110
|
})
|
|
2085
2111
|
.catch((err) => {
|
|
2086
2112
|
state.installedModelsErrorMsg = `Failed to disable: ${err.message}`
|
|
2087
|
-
|
|
2113
|
+
scheduleErrorMsgClear('installedModelsErrorMsg', 3000)
|
|
2088
2114
|
})
|
|
2089
2115
|
return
|
|
2090
2116
|
}
|
|
@@ -2099,8 +2125,6 @@ export function createKeyHandler(ctx) {
|
|
|
2099
2125
|
// 📖 Incompatible fallback overlay: ↑↓ navigate across tool + model sections, Enter confirms, Esc cancels.
|
|
2100
2126
|
// 📖 Cursor is a flat index: 0..N-1 = compatible tools, N..N+M-1 = similar models.
|
|
2101
2127
|
if (state.incompatibleFallbackOpen) {
|
|
2102
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
2103
|
-
|
|
2104
2128
|
const tools = state.incompatibleFallbackTools || []
|
|
2105
2129
|
const similarModels = state.incompatibleFallbackSimilarModels || []
|
|
2106
2130
|
const totalItems = tools.length + similarModels.length
|
|
@@ -2189,14 +2213,12 @@ export function createKeyHandler(ctx) {
|
|
|
2189
2213
|
if (key.name === 'pageup') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - pageStep); return }
|
|
2190
2214
|
if (key.name === 'pagedown') { state.helpScrollOffset += pageStep; return }
|
|
2191
2215
|
if (key.name === 'home') { state.helpScrollOffset = 0; return }
|
|
2192
|
-
if (key.name === 'end') { state.helpScrollOffset =
|
|
2193
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
2216
|
+
if (key.name === 'end') { state.helpScrollOffset = OVERLAY_SCROLL_END_SENTINEL; return }
|
|
2194
2217
|
return
|
|
2195
2218
|
}
|
|
2196
2219
|
|
|
2197
2220
|
// 📖 Token Usage overlay: Shift+T shows token history chart and today/all-time breakdowns.
|
|
2198
2221
|
if (state.tokenUsageOpen) {
|
|
2199
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
2200
2222
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
|
|
2201
2223
|
if (key.name === 'escape') {
|
|
2202
2224
|
closeTokenUsageOverlay()
|
|
@@ -2223,7 +2245,7 @@ export function createKeyHandler(ctx) {
|
|
|
2223
2245
|
return
|
|
2224
2246
|
}
|
|
2225
2247
|
if (key.name === 'end') {
|
|
2226
|
-
state.tokenUsageScrollOffset =
|
|
2248
|
+
state.tokenUsageScrollOffset = OVERLAY_SCROLL_END_SENTINEL
|
|
2227
2249
|
return
|
|
2228
2250
|
}
|
|
2229
2251
|
return
|
|
@@ -2232,7 +2254,6 @@ export function createKeyHandler(ctx) {
|
|
|
2232
2254
|
// 📖 Runtime Report overlay (t3): Shift+W. Per-model real-world breakdown +
|
|
2233
2255
|
// 📖 recent calls. Scrolling with up/down/pageup/pagedown/escape.
|
|
2234
2256
|
if (state.runtimeReportOpen) {
|
|
2235
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
2236
2257
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
|
|
2237
2258
|
if (key.name === 'escape') {
|
|
2238
2259
|
closeRuntimeReportOverlay()
|
|
@@ -2259,114 +2280,22 @@ export function createKeyHandler(ctx) {
|
|
|
2259
2280
|
return
|
|
2260
2281
|
}
|
|
2261
2282
|
if (key.name === 'end') {
|
|
2262
|
-
state.runtimeReportScrollOffset =
|
|
2283
|
+
state.runtimeReportScrollOffset = OVERLAY_SCROLL_END_SENTINEL
|
|
2263
2284
|
return
|
|
2264
2285
|
}
|
|
2265
2286
|
return
|
|
2266
2287
|
}
|
|
2267
2288
|
|
|
2268
|
-
// 📖 Router Onboarding overlay
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
state.routerOnboardingOpen = false
|
|
2274
|
-
// 📖 Mark onboarding as seen (don't show again)
|
|
2275
|
-
if (state.config?.router) {
|
|
2276
|
-
state.config.router.onboardingSeen = true
|
|
2277
|
-
}
|
|
2278
|
-
return
|
|
2279
|
-
}
|
|
2280
|
-
return
|
|
2281
|
-
}
|
|
2282
|
-
if (key.name === 'escape' || key.name === 'n') {
|
|
2283
|
-
state.routerOnboardingOpen = false
|
|
2284
|
-
// 📖 Mark as seen and disabled
|
|
2285
|
-
if (state.config?.router) {
|
|
2286
|
-
state.config.router.onboardingSeen = true
|
|
2287
|
-
state.config.router.enabled = false
|
|
2288
|
-
}
|
|
2289
|
-
return
|
|
2290
|
-
}
|
|
2291
|
-
if (key.name === 'up' || key.name === 'k') {
|
|
2292
|
-
state.routerOnboardingCursor = 0
|
|
2293
|
-
return
|
|
2294
|
-
}
|
|
2295
|
-
if (key.name === 'down' || key.name === 'j') {
|
|
2296
|
-
state.routerOnboardingCursor = 1
|
|
2297
|
-
return
|
|
2298
|
-
}
|
|
2299
|
-
if (key.name === 'return' || key.name === 'y') {
|
|
2300
|
-
const shouldEnable = key.name === 'return' ? true : (state.routerOnboardingCursor === 0)
|
|
2301
|
-
if (!shouldEnable) {
|
|
2302
|
-
state.routerOnboardingOpen = false
|
|
2303
|
-
if (state.config?.router) {
|
|
2304
|
-
state.config.router.onboardingSeen = true
|
|
2305
|
-
state.config.router.enabled = false
|
|
2306
|
-
}
|
|
2307
|
-
return
|
|
2308
|
-
}
|
|
2309
|
-
// 📖 Enable router: start daemon in background and mark onboarding seen
|
|
2310
|
-
state.routerOnboardingPhase = 'loading'
|
|
2311
|
-
state.routerOnboardingError = null
|
|
2312
|
-
void (async () => {
|
|
2313
|
-
try {
|
|
2314
|
-
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
2315
|
-
const child = spawn('node', [binPath, '--daemon-bg'], {
|
|
2316
|
-
detached: true,
|
|
2317
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2318
|
-
env: { ...process.env, ...(isPackageDevMode() && !process.env.FCM_DEV ? { FCM_DEV: '1' } : {}) },
|
|
2319
|
-
})
|
|
2320
|
-
child.unref()
|
|
2321
|
-
|
|
2322
|
-
// 📖 Capture daemon startup output so we can show the real error reason
|
|
2323
|
-
let onboardingStdout = ''
|
|
2324
|
-
let onboardingStderr = ''
|
|
2325
|
-
child.stdout?.on('data', (chunk) => { onboardingStdout += chunk })
|
|
2326
|
-
child.stderr?.on('data', (chunk) => { onboardingStderr += chunk })
|
|
2327
|
-
child.on('close', (code) => {
|
|
2328
|
-
if (code === 0) return
|
|
2329
|
-
let detail = null
|
|
2330
|
-
try {
|
|
2331
|
-
const parsed = JSON.parse(onboardingStdout.trim())
|
|
2332
|
-
if (parsed?.error) detail = parsed.error
|
|
2333
|
-
} catch {}
|
|
2334
|
-
if (!detail && onboardingStderr.trim()) {
|
|
2335
|
-
const lines = onboardingStderr.trim().split('\n').filter(l => !l.startsWith('node:'))
|
|
2336
|
-
detail = lines.slice(0, 2).join(' — ') || onboardingStderr.trim().slice(0, 120)
|
|
2337
|
-
}
|
|
2338
|
-
if (state.routerOnboardingPhase === 'loading') {
|
|
2339
|
-
state.routerOnboardingPhase = 'error'
|
|
2340
|
-
state.routerOnboardingError = detail || `Daemon exited with code ${code}`
|
|
2341
|
-
}
|
|
2342
|
-
})
|
|
2343
|
-
await new Promise((r) => setTimeout(r, 2000))
|
|
2344
|
-
if (state.routerOnboardingPhase === 'loading') {
|
|
2345
|
-
state.routerOnboardingPhase = 'success'
|
|
2346
|
-
if (state.config?.router) {
|
|
2347
|
-
state.config.router.enabled = true
|
|
2348
|
-
state.config.router.onboardingSeen = true
|
|
2349
|
-
saveConfig(state.config)
|
|
2350
|
-
}
|
|
2351
|
-
trackTelemetryEvent('app_router_install', { router_version: '0.4.0' })
|
|
2352
|
-
await new Promise((r) => setTimeout(r, 1500))
|
|
2353
|
-
state.routerOnboardingOpen = false
|
|
2354
|
-
openRouterDashboardOverlay(state)
|
|
2355
|
-
}
|
|
2356
|
-
} catch (err) {
|
|
2357
|
-
state.routerOnboardingPhase = 'error'
|
|
2358
|
-
state.routerOnboardingError = err?.message || 'Failed to start router'
|
|
2359
|
-
}
|
|
2360
|
-
})()
|
|
2361
|
-
return
|
|
2362
|
-
}
|
|
2363
|
-
return
|
|
2364
|
-
}
|
|
2289
|
+
// 📖 Router Onboarding overlay removed (dead code): nothing ever set
|
|
2290
|
+
// 📖 state.routerOnboardingOpen = true (the TUI now auto-enables the router
|
|
2291
|
+
// 📖 silently at startup, see app.js), so this block and its renderer were
|
|
2292
|
+
// 📖 unreachable. The config.router.onboardingSeen writers below are gone
|
|
2293
|
+
// 📖 with it; app.js still flips the flags on boot.
|
|
2365
2294
|
|
|
2366
2295
|
// 📖 Changelog overlay: two-phase (index + details) with keyboard navigation
|
|
2367
2296
|
if (state.changelogOpen) {
|
|
2368
2297
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
|
|
2369
|
-
const changelogData =
|
|
2298
|
+
const changelogData = loadChangelogCached()
|
|
2370
2299
|
const { versions } = changelogData
|
|
2371
2300
|
const versionList = Object.keys(versions).sort((a, b) => {
|
|
2372
2301
|
const aParts = a.split('.').map(Number)
|
|
@@ -2476,14 +2405,11 @@ export function createKeyHandler(ctx) {
|
|
|
2476
2405
|
if (key.name === 'end') { state.changelogScrollOffset = maxScrollOffset; return }
|
|
2477
2406
|
}
|
|
2478
2407
|
|
|
2479
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
2480
2408
|
return
|
|
2481
2409
|
}
|
|
2482
2410
|
|
|
2483
2411
|
// 📖 Smart Recommend overlay: full keyboard handling while overlay is open.
|
|
2484
2412
|
if (state.recommendOpen) {
|
|
2485
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
2486
|
-
|
|
2487
2413
|
if (state.recommendPhase === 'questionnaire') {
|
|
2488
2414
|
const questions = [
|
|
2489
2415
|
{ options: Object.keys(TASK_TYPES), answerKey: 'taskType' },
|
|
@@ -2615,7 +2541,7 @@ export function createKeyHandler(ctx) {
|
|
|
2615
2541
|
state.settingsAddKeyMode = false
|
|
2616
2542
|
state.settingsEditBuffer = ''
|
|
2617
2543
|
state.settingsErrorMsg = '⚠️ OpenRouter keys must start with "sk-or-". Key not saved.'
|
|
2618
|
-
|
|
2544
|
+
scheduleErrorMsgClear('settingsErrorMsg', 3000)
|
|
2619
2545
|
return
|
|
2620
2546
|
}
|
|
2621
2547
|
if (!state.config.apiKeys || typeof state.config.apiKeys !== 'object' || Array.isArray(state.config.apiKeys)) {
|
|
@@ -2634,7 +2560,7 @@ export function createKeyHandler(ctx) {
|
|
|
2634
2560
|
const saveResult = persistApiKeysForProvider(state.config, pk)
|
|
2635
2561
|
if (!saveResult.success) {
|
|
2636
2562
|
state.settingsErrorMsg = `⚠️ Failed to persist ${pk} API key: ${saveResult.error || 'Unknown error'}`
|
|
2637
|
-
|
|
2563
|
+
scheduleErrorMsgClear('settingsErrorMsg', 4000)
|
|
2638
2564
|
} else {
|
|
2639
2565
|
trackAppAction('api_key_saved', {
|
|
2640
2566
|
provider_key: pk,
|
|
@@ -2667,6 +2593,7 @@ export function createKeyHandler(ctx) {
|
|
|
2667
2593
|
state.settingsAddKeyMode = false
|
|
2668
2594
|
state.settingsEditBuffer = ''
|
|
2669
2595
|
state.settingsSyncStatus = null // 📖 Clear sync status on close
|
|
2596
|
+
clearErrorMsgTimer('settingsErrorMsg') // 📖 Pending auto-clear must not fire into a closed overlay
|
|
2670
2597
|
// 📖 Rebuild results: add models from newly enabled providers, remove disabled
|
|
2671
2598
|
const nextResults = MODELS
|
|
2672
2599
|
.filter(([,,,,,pk]) => isProviderEnabled(state.config, pk))
|
|
@@ -2868,8 +2795,6 @@ export function createKeyHandler(ctx) {
|
|
|
2868
2795
|
|
|
2869
2796
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
2870
2797
|
|
|
2871
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
2872
|
-
|
|
2873
2798
|
// 📖 + key: open add-key input (empty buffer) — appends new key on Enter
|
|
2874
2799
|
if ((str === '+' || key.name === '+') && state.settingsCursor < providerKeys.length) {
|
|
2875
2800
|
state.settingsEditBuffer = '' // 📖 Start with empty buffer (not existing key)
|
|
@@ -3014,7 +2939,7 @@ export function createKeyHandler(ctx) {
|
|
|
3014
2939
|
return
|
|
3015
2940
|
}
|
|
3016
2941
|
|
|
3017
|
-
// 📖 Sorting keys: R=rank, O=origin, M=model, L=latest ping, A=avg ping, S=SWE-bench, C=context, H=health, V=verdict, B=stability, U=uptime,
|
|
2942
|
+
// 📖 Sorting keys: R=rank, O=origin, M=model, L=latest ping, A=avg ping, S=SWE-bench, C=context, H=health, V=verdict, B=stability, U=uptime (G cycles theme, not a sort key)
|
|
3018
2943
|
// 📖 T is reserved for tier filter cycling. Y toggles favorites display mode.
|
|
3019
2944
|
// 📖 X clears the active custom text filter.
|
|
3020
2945
|
// 📖 D is now reserved for provider filter cycling
|
|
@@ -3278,6 +3203,17 @@ export function createMouseEventHandler(ctx) {
|
|
|
3278
3203
|
cycleToolMode,
|
|
3279
3204
|
} = ctx
|
|
3280
3205
|
|
|
3206
|
+
// 📖 Shared helper: recompute the visible+sorted list from current filters/sort.
|
|
3207
|
+
// 📖 Hoisted (P3 dedup): this exact block was copy-pasted in
|
|
3208
|
+
// 📖 setSortColumnFromClick, toggleFavoriteAtRow and the Tier header click.
|
|
3209
|
+
function refreshVisibleSorted() {
|
|
3210
|
+
const visible = state.results.filter(r => !r.hidden)
|
|
3211
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
3212
|
+
pinFavorites: state.favoritesPinnedAndSticky,
|
|
3213
|
+
benchmarkResults: state.benchmarkResults,
|
|
3214
|
+
})
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3281
3217
|
// 📖 Shared helper: set the sort column, toggling direction if same column clicked twice.
|
|
3282
3218
|
function setSortColumnFromClick(col) {
|
|
3283
3219
|
if (state.sortColumn === col) {
|
|
@@ -3290,20 +3226,15 @@ export function createMouseEventHandler(ctx) {
|
|
|
3290
3226
|
state.headerFlashColumn = col
|
|
3291
3227
|
state.headerFlashUntilFrame = state.frame + 3
|
|
3292
3228
|
// 📖 Recompute visible sorted list to reflect new sort order
|
|
3293
|
-
|
|
3294
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
3295
|
-
pinFavorites: state.favoritesPinnedAndSticky,
|
|
3296
|
-
benchmarkResults: state.benchmarkResults,
|
|
3297
|
-
})
|
|
3229
|
+
refreshVisibleSorted()
|
|
3298
3230
|
}
|
|
3299
3231
|
|
|
3300
|
-
// 📖 Shared helper: persist UI settings after mouse-triggered changes
|
|
3232
|
+
// 📖 Shared helper: persist UI settings after mouse-triggered changes.
|
|
3233
|
+
// 📖 Delegates to the module-level helper shared with the keyboard path so a
|
|
3234
|
+
// 📖 mouse sort persists via sortAsc + saveConfig (the old copy wrote a
|
|
3235
|
+
// 📖 sortDirection field nothing reads and never saved).
|
|
3301
3236
|
function persistUiSettings() {
|
|
3302
|
-
|
|
3303
|
-
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
3304
|
-
state.config.settings.sortColumn = state.sortColumn
|
|
3305
|
-
state.config.settings.sortDirection = state.sortDirection
|
|
3306
|
-
state.config.settings.tierFilter = TIER_CYCLE[state.tierFilterMode] || null
|
|
3237
|
+
persistTableViewSettings(state, { TIER_CYCLE, saveConfig })
|
|
3307
3238
|
}
|
|
3308
3239
|
|
|
3309
3240
|
// 📖 Shared helper: toggle favorite on a specific model row index.
|
|
@@ -3315,11 +3246,7 @@ export function createMouseEventHandler(ctx) {
|
|
|
3315
3246
|
toggleFavoriteModel(state.config, selected.providerKey, selected.modelId)
|
|
3316
3247
|
syncFavoriteFlags(state.results, state.config)
|
|
3317
3248
|
applyTierFilter()
|
|
3318
|
-
|
|
3319
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
3320
|
-
pinFavorites: state.favoritesPinnedAndSticky,
|
|
3321
|
-
benchmarkResults: state.benchmarkResults,
|
|
3322
|
-
})
|
|
3249
|
+
refreshVisibleSorted()
|
|
3323
3250
|
// 📖 If we unfavorited while pinned mode is on, reset cursor to top
|
|
3324
3251
|
if (wasFavorite && state.favoritesPinnedAndSticky) {
|
|
3325
3252
|
state.cursor = 0
|
|
@@ -3633,11 +3560,7 @@ export function createMouseEventHandler(ctx) {
|
|
|
3633
3560
|
state.headerFlashUntilFrame = state.frame + 3
|
|
3634
3561
|
state.tierFilterMode = (state.tierFilterMode + 1) % TIER_CYCLE.length
|
|
3635
3562
|
applyTierFilter()
|
|
3636
|
-
|
|
3637
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
3638
|
-
pinFavorites: state.favoritesPinnedAndSticky,
|
|
3639
|
-
benchmarkResults: state.benchmarkResults,
|
|
3640
|
-
})
|
|
3563
|
+
refreshVisibleSorted()
|
|
3641
3564
|
state.cursor = 0
|
|
3642
3565
|
state.scrollOffset = 0
|
|
3643
3566
|
persistUiSettings()
|
|
@@ -3649,7 +3572,20 @@ export function createMouseEventHandler(ctx) {
|
|
|
3649
3572
|
// 📖 Check if click is on a model row → move cursor (or select on double-click)
|
|
3650
3573
|
// 📖 Right-click toggles favorite on that row (same as F key)
|
|
3651
3574
|
if (y >= layout.firstModelRow && y <= layout.lastModelRow) {
|
|
3652
|
-
|
|
3575
|
+
// 📖 Space-expanded detail card (fix 15): the 2 extra lines under the
|
|
3576
|
+
// 📖 expanded row shift every row below it down, so a raw y - firstModelRow
|
|
3577
|
+
// 📖 offset pointed 2 models too far. Shift offsets below the card back up;
|
|
3578
|
+
// 📖 a click ON the card lines selects the expanded row itself.
|
|
3579
|
+
let rowOffset = y - layout.firstModelRow
|
|
3580
|
+
const expandedDetailRows = layout.expandedDetailRows || 0
|
|
3581
|
+
if (expandedDetailRows > 0) {
|
|
3582
|
+
const expandedRel = (layout.expandedDetailAfterIdx ?? -1) - layout.viewportStartIdx
|
|
3583
|
+
if (rowOffset > expandedRel && rowOffset <= expandedRel + expandedDetailRows) {
|
|
3584
|
+
rowOffset = expandedRel
|
|
3585
|
+
} else if (rowOffset > expandedRel) {
|
|
3586
|
+
rowOffset -= expandedDetailRows
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3653
3589
|
const modelIdx = layout.viewportStartIdx + rowOffset
|
|
3654
3590
|
if (modelIdx >= layout.viewportStartIdx && modelIdx < layout.viewportEndIdx) {
|
|
3655
3591
|
state.cursor = modelIdx
|
|
@@ -3678,9 +3614,17 @@ export function createMouseEventHandler(ctx) {
|
|
|
3678
3614
|
return
|
|
3679
3615
|
}
|
|
3680
3616
|
// 📖 Map the footer zone key to a synthetic keypress.
|
|
3681
|
-
// 📖 Most are single-character keys;
|
|
3617
|
+
// 📖 Most are single-character keys; ctrl-modified zones emit ctrl keys so
|
|
3618
|
+
// 📖 they hit the real shortcut handlers (plain 'a'/'u' are SORT keys and
|
|
3619
|
+
// 📖 the old zones silently re-sorted the table instead of benchmarking).
|
|
3682
3620
|
if (zone.key === 'ctrl+p') {
|
|
3683
3621
|
process.stdin.emit('keypress', '\x10', { name: 'p', ctrl: true, meta: false, shift: false })
|
|
3622
|
+
} else if (zone.key === 'ctrl+a') {
|
|
3623
|
+
// 📖 "AI Speed Test" badge → Ctrl+A (benchmark selected model)
|
|
3624
|
+
process.stdin.emit('keypress', '\x01', { name: 'a', ctrl: true, meta: false, shift: false })
|
|
3625
|
+
} else if (zone.key === 'ctrl+u') {
|
|
3626
|
+
// 📖 "Global AI Speed Test" badge → Ctrl+U (benchmark all models)
|
|
3627
|
+
process.stdin.emit('keypress', '\x15', { name: 'u', ctrl: true, meta: false, shift: false })
|
|
3684
3628
|
} else if (zone.key === 'shift+r') {
|
|
3685
3629
|
process.stdin.emit('keypress', 'R', { name: 'r', ctrl: false, meta: false, shift: true })
|
|
3686
3630
|
} else {
|