free-coding-models 0.3.24 → 0.3.26
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/CHANGELOG.md +51 -0
- package/README.md +20 -2
- package/package.json +1 -1
- package/sources.js +37 -19
- package/src/app.js +84 -2
- package/src/command-palette.js +1 -0
- package/src/constants.js +5 -2
- package/src/installed-models-manager.js +636 -0
- package/src/key-handler.js +548 -0
- package/src/mouse.js +186 -0
- package/src/overlays.js +170 -1
- package/src/render-table.js +169 -42
package/src/key-handler.js
CHANGED
|
@@ -31,9 +31,12 @@ import { loadChangelog } from './changelog-loader.js'
|
|
|
31
31
|
import { getToolMeta, isModelCompatibleWithTool, getCompatibleTools, findSimilarCompatibleModels } from './tool-metadata.js'
|
|
32
32
|
import { loadConfig, replaceConfigContents } from './config.js'
|
|
33
33
|
import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
|
|
34
|
+
import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
|
|
34
35
|
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
35
36
|
import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntries } from './command-palette.js'
|
|
36
37
|
import { WIDTH_WARNING_MIN_COLS } from './constants.js'
|
|
38
|
+
import { scanAllToolConfigs, softDeleteModel } from './installed-models-manager.js'
|
|
39
|
+
import { startExternalTool } from './tool-launchers.js'
|
|
37
40
|
|
|
38
41
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
39
42
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
@@ -686,6 +689,21 @@ export function createKeyHandler(ctx) {
|
|
|
686
689
|
state.changelogSelectedVersion = null
|
|
687
690
|
}
|
|
688
691
|
|
|
692
|
+
function openInstalledModelsOverlay() {
|
|
693
|
+
state.installedModelsOpen = true
|
|
694
|
+
state.installedModelsCursor = 0
|
|
695
|
+
state.installedModelsScrollOffset = 0
|
|
696
|
+
state.installedModelsErrorMsg = 'Scanning...'
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
const results = scanAllToolConfigs()
|
|
700
|
+
state.installedModelsData = results
|
|
701
|
+
state.installedModelsErrorMsg = null
|
|
702
|
+
} catch (err) {
|
|
703
|
+
state.installedModelsErrorMsg = err.message || 'Failed to scan tool configs'
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
689
707
|
function cycleToolMode() {
|
|
690
708
|
const modeOrder = getToolModeOrder()
|
|
691
709
|
const currentIndex = modeOrder.indexOf(state.mode)
|
|
@@ -773,6 +791,7 @@ export function createKeyHandler(ctx) {
|
|
|
773
791
|
return state.settingsOpen
|
|
774
792
|
|| state.installEndpointsOpen
|
|
775
793
|
|| state.toolInstallPromptOpen
|
|
794
|
+
|| state.installedModelsOpen
|
|
776
795
|
|| state.recommendOpen
|
|
777
796
|
|| state.feedbackOpen
|
|
778
797
|
|| state.helpVisible
|
|
@@ -942,6 +961,7 @@ export function createKeyHandler(ctx) {
|
|
|
942
961
|
case 'open-feedback': return openFeedbackOverlay()
|
|
943
962
|
case 'open-recommend': return openRecommendOverlay()
|
|
944
963
|
case 'open-install-endpoints': return openInstallEndpointsOverlay()
|
|
964
|
+
case 'open-installed-models': return openInstalledModelsOverlay()
|
|
945
965
|
case 'action-cycle-theme': return cycleGlobalTheme()
|
|
946
966
|
case 'action-cycle-tool-mode': return cycleToolMode()
|
|
947
967
|
case 'action-cycle-ping-mode': {
|
|
@@ -1288,6 +1308,104 @@ export function createKeyHandler(ctx) {
|
|
|
1288
1308
|
return
|
|
1289
1309
|
}
|
|
1290
1310
|
|
|
1311
|
+
// ─── Installed Models overlay keyboard handling ───────────────────────────
|
|
1312
|
+
if (state.installedModelsOpen) {
|
|
1313
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1314
|
+
|
|
1315
|
+
const scanResults = state.installedModelsData || []
|
|
1316
|
+
let maxIndex = 0
|
|
1317
|
+
for (const toolResult of scanResults) {
|
|
1318
|
+
maxIndex += 1
|
|
1319
|
+
maxIndex += toolResult.models.length
|
|
1320
|
+
}
|
|
1321
|
+
if (maxIndex > 0) maxIndex--
|
|
1322
|
+
|
|
1323
|
+
const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
|
|
1324
|
+
|
|
1325
|
+
if (key.name === 'up' || (key.shift && key.name === 'tab')) {
|
|
1326
|
+
state.installedModelsCursor = Math.max(0, state.installedModelsCursor - 1)
|
|
1327
|
+
return
|
|
1328
|
+
}
|
|
1329
|
+
if (key.name === 'down' || key.name === 'tab') {
|
|
1330
|
+
state.installedModelsCursor = Math.min(maxIndex, state.installedModelsCursor + 1)
|
|
1331
|
+
return
|
|
1332
|
+
}
|
|
1333
|
+
if (key.name === 'pageup') {
|
|
1334
|
+
state.installedModelsCursor = Math.max(0, state.installedModelsCursor - pageStep)
|
|
1335
|
+
return
|
|
1336
|
+
}
|
|
1337
|
+
if (key.name === 'pagedown') {
|
|
1338
|
+
state.installedModelsCursor = Math.min(maxIndex, state.installedModelsCursor + pageStep)
|
|
1339
|
+
return
|
|
1340
|
+
}
|
|
1341
|
+
if (key.name === 'home') {
|
|
1342
|
+
state.installedModelsCursor = 0
|
|
1343
|
+
return
|
|
1344
|
+
}
|
|
1345
|
+
if (key.name === 'end') {
|
|
1346
|
+
state.installedModelsCursor = maxIndex
|
|
1347
|
+
return
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
if (key.name === 'escape') {
|
|
1351
|
+
state.installedModelsOpen = false
|
|
1352
|
+
state.installedModelsCursor = 0
|
|
1353
|
+
return
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
if (key.name === 'return') {
|
|
1357
|
+
let currentIdx = 0
|
|
1358
|
+
for (const toolResult of scanResults) {
|
|
1359
|
+
if (currentIdx === state.installedModelsCursor) {
|
|
1360
|
+
return
|
|
1361
|
+
}
|
|
1362
|
+
currentIdx++
|
|
1363
|
+
for (const model of toolResult.models) {
|
|
1364
|
+
if (currentIdx === state.installedModelsCursor) {
|
|
1365
|
+
const selectedModel = {
|
|
1366
|
+
modelId: model.modelId,
|
|
1367
|
+
providerKey: model.providerKey,
|
|
1368
|
+
label: model.label,
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
state.installedModelsOpen = false
|
|
1372
|
+
await startExternalTool(toolResult.toolMode, selectedModel, state.config)
|
|
1373
|
+
return
|
|
1374
|
+
}
|
|
1375
|
+
currentIdx++
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (key.name === 'd') {
|
|
1381
|
+
let currentIdx = 0
|
|
1382
|
+
for (const toolResult of scanResults) {
|
|
1383
|
+
currentIdx++
|
|
1384
|
+
for (const model of toolResult.models) {
|
|
1385
|
+
if (currentIdx === state.installedModelsCursor) {
|
|
1386
|
+
softDeleteModel(toolResult.toolMode, model.modelId)
|
|
1387
|
+
.then((result) => {
|
|
1388
|
+
if (result.success) {
|
|
1389
|
+
openInstalledModelsOverlay()
|
|
1390
|
+
} else {
|
|
1391
|
+
state.installedModelsErrorMsg = `Failed to disable: ${result.error}`
|
|
1392
|
+
setTimeout(() => { state.installedModelsErrorMsg = null }, 3000)
|
|
1393
|
+
}
|
|
1394
|
+
})
|
|
1395
|
+
.catch((err) => {
|
|
1396
|
+
state.installedModelsErrorMsg = `Failed to disable: ${err.message}`
|
|
1397
|
+
setTimeout(() => { state.installedModelsErrorMsg = null }, 3000)
|
|
1398
|
+
})
|
|
1399
|
+
return
|
|
1400
|
+
}
|
|
1401
|
+
currentIdx++
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
return
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1291
1409
|
// 📖 Incompatible fallback overlay: ↑↓ navigate across tool + model sections, Enter confirms, Esc cancels.
|
|
1292
1410
|
// 📖 Cursor is a flat index: 0..N-1 = compatible tools, N..N+M-1 = similar models.
|
|
1293
1411
|
if (state.incompatibleFallbackOpen) {
|
|
@@ -2157,3 +2275,433 @@ export function createKeyHandler(ctx) {
|
|
|
2157
2275
|
}
|
|
2158
2276
|
}
|
|
2159
2277
|
}
|
|
2278
|
+
|
|
2279
|
+
/**
|
|
2280
|
+
* 📖 createMouseEventHandler: Factory that returns a handler for structured mouse events.
|
|
2281
|
+
* 📖 Works alongside the keypress handler — shares the same state and action functions.
|
|
2282
|
+
*
|
|
2283
|
+
* 📖 Supported interactions:
|
|
2284
|
+
* - Click on header row column → sort by that column (or cycle tier filter for Tier column)
|
|
2285
|
+
* - Click on model row → move cursor to that row
|
|
2286
|
+
* - Double-click on model row → select the model (Enter)
|
|
2287
|
+
* - Scroll up/down → navigate cursor up/down (with wrap-around)
|
|
2288
|
+
* - Scroll in overlays → scroll overlay content
|
|
2289
|
+
*
|
|
2290
|
+
* @param {object} ctx — same context object passed to createKeyHandler
|
|
2291
|
+
* @returns {function} — callback for onMouseEvent in createMouseHandler()
|
|
2292
|
+
*/
|
|
2293
|
+
export function createMouseEventHandler(ctx) {
|
|
2294
|
+
const {
|
|
2295
|
+
state,
|
|
2296
|
+
adjustScrollOffset,
|
|
2297
|
+
applyTierFilter,
|
|
2298
|
+
TIER_CYCLE,
|
|
2299
|
+
noteUserActivity,
|
|
2300
|
+
sortResultsWithPinnedFavorites,
|
|
2301
|
+
saveConfig,
|
|
2302
|
+
overlayLayout,
|
|
2303
|
+
// 📖 Favorite toggle deps — used by right-click on model rows
|
|
2304
|
+
toggleFavoriteModel,
|
|
2305
|
+
syncFavoriteFlags,
|
|
2306
|
+
toFavoriteKey,
|
|
2307
|
+
// 📖 Tool mode cycling — used by compat column header click
|
|
2308
|
+
cycleToolMode,
|
|
2309
|
+
} = ctx
|
|
2310
|
+
|
|
2311
|
+
// 📖 Shared helper: set the sort column, toggling direction if same column clicked twice.
|
|
2312
|
+
function setSortColumnFromClick(col) {
|
|
2313
|
+
if (state.sortColumn === col) {
|
|
2314
|
+
state.sortDirection = state.sortDirection === 'asc' ? 'desc' : 'asc'
|
|
2315
|
+
} else {
|
|
2316
|
+
state.sortColumn = col
|
|
2317
|
+
state.sortDirection = 'asc'
|
|
2318
|
+
}
|
|
2319
|
+
// 📖 Recompute visible sorted list to reflect new sort order
|
|
2320
|
+
const visible = state.results.filter(r => !r.hidden)
|
|
2321
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
2322
|
+
pinFavorites: state.favoritesPinnedAndSticky,
|
|
2323
|
+
})
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// 📖 Shared helper: persist UI settings after mouse-triggered changes
|
|
2327
|
+
function persistUiSettings() {
|
|
2328
|
+
if (!state.config) return
|
|
2329
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
2330
|
+
state.config.settings.sortColumn = state.sortColumn
|
|
2331
|
+
state.config.settings.sortDirection = state.sortDirection
|
|
2332
|
+
state.config.settings.tierFilter = TIER_CYCLE[state.tierFilterMode] || null
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// 📖 Shared helper: toggle favorite on a specific model row index.
|
|
2336
|
+
// 📖 Mirrors the keyboard F-key handler but operates at a given index.
|
|
2337
|
+
function toggleFavoriteAtRow(modelIdx) {
|
|
2338
|
+
const selected = state.visibleSorted[modelIdx]
|
|
2339
|
+
if (!selected) return
|
|
2340
|
+
const wasFavorite = selected.isFavorite
|
|
2341
|
+
toggleFavoriteModel(state.config, selected.providerKey, selected.modelId)
|
|
2342
|
+
syncFavoriteFlags(state.results, state.config)
|
|
2343
|
+
applyTierFilter()
|
|
2344
|
+
const visible = state.results.filter(r => !r.hidden)
|
|
2345
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
2346
|
+
pinFavorites: state.favoritesPinnedAndSticky,
|
|
2347
|
+
})
|
|
2348
|
+
// 📖 If we unfavorited while pinned mode is on, reset cursor to top
|
|
2349
|
+
if (wasFavorite && state.favoritesPinnedAndSticky) {
|
|
2350
|
+
state.cursor = 0
|
|
2351
|
+
state.scrollOffset = 0
|
|
2352
|
+
return
|
|
2353
|
+
}
|
|
2354
|
+
// 📖 Otherwise, track the model's new position after re-sort
|
|
2355
|
+
const selectedKey = toFavoriteKey(selected.providerKey, selected.modelId)
|
|
2356
|
+
const newCursor = state.visibleSorted.findIndex(r => toFavoriteKey(r.providerKey, r.modelId) === selectedKey)
|
|
2357
|
+
if (newCursor >= 0) state.cursor = newCursor
|
|
2358
|
+
adjustScrollOffset(state)
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// 📖 Shared helper: map a terminal row (1-based) to a cursor index using
|
|
2362
|
+
// 📖 an overlay's cursorLineByRow map and scroll offset.
|
|
2363
|
+
// 📖 Returns the cursor index, or -1 if no match.
|
|
2364
|
+
function overlayRowToCursor(y, cursorToLineMap, scrollOffset) {
|
|
2365
|
+
// 📖 Terminal row Y (1-based) → line index in the overlay lines array.
|
|
2366
|
+
// 📖 sliceOverlayLines shows lines from [scrollOffset .. scrollOffset + terminalRows).
|
|
2367
|
+
// 📖 Terminal row 1 = line[scrollOffset], row 2 = line[scrollOffset+1], etc.
|
|
2368
|
+
const lineIdx = (y - 1) + scrollOffset
|
|
2369
|
+
for (const [cursorStr, lineNum] of Object.entries(cursorToLineMap)) {
|
|
2370
|
+
if (lineNum === lineIdx) return parseInt(cursorStr, 10)
|
|
2371
|
+
}
|
|
2372
|
+
return -1
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
return (evt) => {
|
|
2376
|
+
noteUserActivity()
|
|
2377
|
+
const layout = getLastLayout()
|
|
2378
|
+
|
|
2379
|
+
// ── Scroll events ──────────────────────────────────────────────────
|
|
2380
|
+
if (evt.type === 'scroll-up' || evt.type === 'scroll-down') {
|
|
2381
|
+
// 📖 Overlay scroll: if any overlay is open, scroll its content
|
|
2382
|
+
if (state.helpVisible) {
|
|
2383
|
+
const step = evt.type === 'scroll-up' ? -3 : 3
|
|
2384
|
+
state.helpScrollOffset = Math.max(0, (state.helpScrollOffset || 0) + step)
|
|
2385
|
+
return
|
|
2386
|
+
}
|
|
2387
|
+
if (state.changelogOpen) {
|
|
2388
|
+
const step = evt.type === 'scroll-up' ? -3 : 3
|
|
2389
|
+
state.changelogScrollOffset = Math.max(0, (state.changelogScrollOffset || 0) + step)
|
|
2390
|
+
return
|
|
2391
|
+
}
|
|
2392
|
+
if (state.settingsOpen) {
|
|
2393
|
+
// 📖 Settings overlay uses cursor navigation, not scroll offset.
|
|
2394
|
+
// 📖 Move settingsCursor up/down instead of scrolling.
|
|
2395
|
+
if (evt.type === 'scroll-up') {
|
|
2396
|
+
state.settingsCursor = Math.max(0, (state.settingsCursor || 0) - 1)
|
|
2397
|
+
} else {
|
|
2398
|
+
const max = overlayLayout?.settingsMaxRow ?? 99
|
|
2399
|
+
state.settingsCursor = Math.min(max, (state.settingsCursor || 0) + 1)
|
|
2400
|
+
}
|
|
2401
|
+
return
|
|
2402
|
+
}
|
|
2403
|
+
if (state.recommendOpen) {
|
|
2404
|
+
// 📖 Recommend questionnaire phase: scroll moves cursor through options
|
|
2405
|
+
if (state.recommendPhase === 'questionnaire') {
|
|
2406
|
+
const step = evt.type === 'scroll-up' ? -1 : 1
|
|
2407
|
+
state.recommendCursor = Math.max(0, (state.recommendCursor || 0) + step)
|
|
2408
|
+
} else {
|
|
2409
|
+
const step = evt.type === 'scroll-up' ? -1 : 1
|
|
2410
|
+
state.recommendScrollOffset = Math.max(0, (state.recommendScrollOffset || 0) + step)
|
|
2411
|
+
}
|
|
2412
|
+
return
|
|
2413
|
+
}
|
|
2414
|
+
if (state.feedbackOpen) {
|
|
2415
|
+
// 📖 Feedback overlay doesn't scroll — ignore
|
|
2416
|
+
return
|
|
2417
|
+
}
|
|
2418
|
+
if (state.commandPaletteOpen) {
|
|
2419
|
+
// 📖 Command palette: scroll the results list
|
|
2420
|
+
const count = state.commandPaletteResults?.length || 0
|
|
2421
|
+
if (count === 0) return
|
|
2422
|
+
if (evt.type === 'scroll-up') {
|
|
2423
|
+
state.commandPaletteCursor = state.commandPaletteCursor > 0 ? state.commandPaletteCursor - 1 : count - 1
|
|
2424
|
+
} else {
|
|
2425
|
+
state.commandPaletteCursor = state.commandPaletteCursor < count - 1 ? state.commandPaletteCursor + 1 : 0
|
|
2426
|
+
}
|
|
2427
|
+
return
|
|
2428
|
+
}
|
|
2429
|
+
if (state.installEndpointsOpen) {
|
|
2430
|
+
// 📖 Install endpoints: move cursor up/down
|
|
2431
|
+
if (evt.type === 'scroll-up') {
|
|
2432
|
+
state.installEndpointsCursor = Math.max(0, (state.installEndpointsCursor || 0) - 1)
|
|
2433
|
+
} else {
|
|
2434
|
+
state.installEndpointsCursor = (state.installEndpointsCursor || 0) + 1
|
|
2435
|
+
}
|
|
2436
|
+
return
|
|
2437
|
+
}
|
|
2438
|
+
if (state.toolInstallPromptOpen) {
|
|
2439
|
+
// 📖 Tool install prompt: move cursor up/down
|
|
2440
|
+
if (evt.type === 'scroll-up') {
|
|
2441
|
+
state.toolInstallPromptCursor = Math.max(0, (state.toolInstallPromptCursor || 0) - 1)
|
|
2442
|
+
} else {
|
|
2443
|
+
state.toolInstallPromptCursor = (state.toolInstallPromptCursor || 0) + 1
|
|
2444
|
+
}
|
|
2445
|
+
return
|
|
2446
|
+
}
|
|
2447
|
+
if (state.installedModelsOpen) {
|
|
2448
|
+
const scanResults = state.installedModelsData || []
|
|
2449
|
+
let maxIndex = 0
|
|
2450
|
+
for (const toolResult of scanResults) {
|
|
2451
|
+
maxIndex += 1
|
|
2452
|
+
maxIndex += toolResult.models.length
|
|
2453
|
+
}
|
|
2454
|
+
if (maxIndex > 0) maxIndex--
|
|
2455
|
+
|
|
2456
|
+
if (evt.type === 'scroll-up') {
|
|
2457
|
+
state.installedModelsCursor = Math.max(0, (state.installedModelsCursor || 0) - 1)
|
|
2458
|
+
} else {
|
|
2459
|
+
state.installedModelsCursor = Math.min(maxIndex, (state.installedModelsCursor || 0) + 1)
|
|
2460
|
+
}
|
|
2461
|
+
return
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
// 📖 Main table scroll: move cursor up/down with wrap-around
|
|
2465
|
+
const count = state.visibleSorted.length
|
|
2466
|
+
if (count === 0) return
|
|
2467
|
+
if (evt.type === 'scroll-up') {
|
|
2468
|
+
state.cursor = state.cursor > 0 ? state.cursor - 1 : count - 1
|
|
2469
|
+
} else {
|
|
2470
|
+
state.cursor = state.cursor < count - 1 ? state.cursor + 1 : 0
|
|
2471
|
+
}
|
|
2472
|
+
adjustScrollOffset(state)
|
|
2473
|
+
return
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// ── Click / double-click events ────────────────────────────────────
|
|
2477
|
+
if (evt.type !== 'click' && evt.type !== 'double-click') return
|
|
2478
|
+
|
|
2479
|
+
const { x, y } = evt
|
|
2480
|
+
|
|
2481
|
+
// ── Overlay click handling ─────────────────────────────────────────
|
|
2482
|
+
// 📖 When an overlay is open, handle clicks inside it or close it.
|
|
2483
|
+
// 📖 Priority order matches the rendering priority in app.js.
|
|
2484
|
+
|
|
2485
|
+
if (state.commandPaletteOpen) {
|
|
2486
|
+
// 📖 Command palette is a floating modal — detect clicks inside vs outside.
|
|
2487
|
+
const cp = overlayLayout
|
|
2488
|
+
const insideModal = cp &&
|
|
2489
|
+
x >= (cp.commandPaletteLeft || 0) && x <= (cp.commandPaletteRight || 0) &&
|
|
2490
|
+
y >= (cp.commandPaletteTop || 0) && y <= (cp.commandPaletteBottom || 0)
|
|
2491
|
+
|
|
2492
|
+
if (insideModal) {
|
|
2493
|
+
// 📖 Check if click is in the body area (result rows)
|
|
2494
|
+
const bodyStart = cp.commandPaletteBodyStartRow || 0
|
|
2495
|
+
const bodyEnd = bodyStart + (cp.commandPaletteBodyRows || 0) - 1
|
|
2496
|
+
if (y >= bodyStart && y <= bodyEnd) {
|
|
2497
|
+
// 📖 Map terminal row → cursor index via the cursorToLine map + scroll offset
|
|
2498
|
+
const cursorIdx = overlayRowToCursor(
|
|
2499
|
+
y - bodyStart + 1, // 📖 Normalize: row within body → 1-based for overlayRowToCursor
|
|
2500
|
+
cp.commandPaletteCursorToLine,
|
|
2501
|
+
cp.commandPaletteScrollOffset
|
|
2502
|
+
)
|
|
2503
|
+
if (cursorIdx >= 0) {
|
|
2504
|
+
state.commandPaletteCursor = cursorIdx
|
|
2505
|
+
if (evt.type === 'double-click') {
|
|
2506
|
+
// 📖 Double-click executes the selected command (same as Enter)
|
|
2507
|
+
process.stdin.emit('keypress', '\r', { name: 'return', ctrl: false, meta: false, shift: false })
|
|
2508
|
+
}
|
|
2509
|
+
return
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
// 📖 Click inside modal but not on a result row — ignore (don't close)
|
|
2513
|
+
return
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// 📖 Click outside the modal → close (Escape equivalent)
|
|
2517
|
+
state.commandPaletteOpen = false
|
|
2518
|
+
state.commandPaletteFrozenTable = null
|
|
2519
|
+
state.commandPaletteQuery = ''
|
|
2520
|
+
state.commandPaletteCursor = 0
|
|
2521
|
+
state.commandPaletteScrollOffset = 0
|
|
2522
|
+
state.commandPaletteResults = []
|
|
2523
|
+
return
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
if (state.installEndpointsOpen) {
|
|
2527
|
+
// 📖 Install endpoints overlay: click closes (Escape equivalent)
|
|
2528
|
+
state.installEndpointsOpen = false
|
|
2529
|
+
return
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
if (state.toolInstallPromptOpen) {
|
|
2533
|
+
// 📖 Tool install prompt: click closes (Escape equivalent)
|
|
2534
|
+
state.toolInstallPromptOpen = false
|
|
2535
|
+
return
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
if (state.installedModelsOpen) {
|
|
2539
|
+
state.installedModelsOpen = false
|
|
2540
|
+
return
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
if (state.incompatibleFallbackOpen) {
|
|
2544
|
+
// 📖 Incompatible fallback: click closes
|
|
2545
|
+
state.incompatibleFallbackOpen = false
|
|
2546
|
+
return
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
if (state.feedbackOpen) {
|
|
2550
|
+
// 📖 Feedback overlay: click anywhere closes (no scroll, no cursor)
|
|
2551
|
+
state.feedbackOpen = false
|
|
2552
|
+
state.feedbackInput = ''
|
|
2553
|
+
return
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
if (state.helpVisible) {
|
|
2557
|
+
// 📖 Help overlay: click anywhere closes (same as K or Escape)
|
|
2558
|
+
state.helpVisible = false
|
|
2559
|
+
return
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
if (state.changelogOpen) {
|
|
2563
|
+
// 📖 Changelog overlay: click on a version row selects it, otherwise close.
|
|
2564
|
+
if (overlayLayout && state.changelogPhase === 'index') {
|
|
2565
|
+
const cursorIdx = overlayRowToCursor(
|
|
2566
|
+
y,
|
|
2567
|
+
overlayLayout.changelogCursorToLine,
|
|
2568
|
+
overlayLayout.changelogScrollOffset
|
|
2569
|
+
)
|
|
2570
|
+
if (cursorIdx >= 0) {
|
|
2571
|
+
state.changelogCursor = cursorIdx
|
|
2572
|
+
// 📖 Double-click opens the selected version's details (same as Enter)
|
|
2573
|
+
if (evt.type === 'double-click') {
|
|
2574
|
+
process.stdin.emit('keypress', '\r', { name: 'return', ctrl: false, meta: false, shift: false })
|
|
2575
|
+
}
|
|
2576
|
+
return
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
// 📖 Click outside version list → close (Escape equivalent)
|
|
2580
|
+
// 📖 In details phase, click anywhere goes back (same as B key)
|
|
2581
|
+
if (state.changelogPhase === 'details') {
|
|
2582
|
+
state.changelogPhase = 'index'
|
|
2583
|
+
state.changelogScrollOffset = 0
|
|
2584
|
+
} else {
|
|
2585
|
+
state.changelogOpen = false
|
|
2586
|
+
}
|
|
2587
|
+
return
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
if (state.recommendOpen) {
|
|
2591
|
+
if (state.recommendPhase === 'questionnaire' && overlayLayout?.recommendOptionRows) {
|
|
2592
|
+
// 📖 Map click Y to the specific questionnaire option row
|
|
2593
|
+
const optRows = overlayLayout.recommendOptionRows
|
|
2594
|
+
for (const [idxStr, row] of Object.entries(optRows)) {
|
|
2595
|
+
if (y === row) {
|
|
2596
|
+
state.recommendCursor = parseInt(idxStr, 10)
|
|
2597
|
+
if (evt.type === 'double-click') {
|
|
2598
|
+
// 📖 Double-click confirms the option (same as Enter)
|
|
2599
|
+
process.stdin.emit('keypress', '\r', { name: 'return', ctrl: false, meta: false, shift: false })
|
|
2600
|
+
}
|
|
2601
|
+
return
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
// 📖 Click outside option rows in questionnaire — ignore (don't close)
|
|
2605
|
+
return
|
|
2606
|
+
}
|
|
2607
|
+
// 📖 Result phase: click closes. Analyzing phase: click does nothing.
|
|
2608
|
+
if (state.recommendPhase === 'results') {
|
|
2609
|
+
state.recommendOpen = false
|
|
2610
|
+
state.recommendPhase = null
|
|
2611
|
+
state.recommendResults = []
|
|
2612
|
+
state.recommendScrollOffset = 0
|
|
2613
|
+
}
|
|
2614
|
+
return
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
if (state.settingsOpen) {
|
|
2618
|
+
// 📖 Settings overlay: click on a provider/maintenance row moves cursor there.
|
|
2619
|
+
// 📖 Don't handle clicks during edit/add-key mode (keyboard is primary).
|
|
2620
|
+
if (state.settingsEditMode || state.settingsAddKeyMode) return
|
|
2621
|
+
|
|
2622
|
+
if (overlayLayout) {
|
|
2623
|
+
const cursorIdx = overlayRowToCursor(
|
|
2624
|
+
y,
|
|
2625
|
+
overlayLayout.settingsCursorToLine,
|
|
2626
|
+
overlayLayout.settingsScrollOffset
|
|
2627
|
+
)
|
|
2628
|
+
if (cursorIdx >= 0 && cursorIdx <= (overlayLayout.settingsMaxRow || 99)) {
|
|
2629
|
+
state.settingsCursor = cursorIdx
|
|
2630
|
+
// 📖 Double-click triggers the Enter action (edit key / toggle / run action)
|
|
2631
|
+
if (evt.type === 'double-click') {
|
|
2632
|
+
process.stdin.emit('keypress', '\r', { name: 'return', ctrl: false, meta: false, shift: false })
|
|
2633
|
+
}
|
|
2634
|
+
return
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
// 📖 Click outside any recognized row does nothing in Settings
|
|
2638
|
+
// 📖 (user can Escape or press P to close)
|
|
2639
|
+
return
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
// ── Main table click handling ──────────────────────────────────────
|
|
2643
|
+
// 📖 No overlay is open — clicks go to the main table.
|
|
2644
|
+
|
|
2645
|
+
// 📖 Check if click is on the column header row → trigger sort
|
|
2646
|
+
if (y === layout.headerRow) {
|
|
2647
|
+
const col = layout.columns.find(c => x >= c.xStart && x <= c.xEnd)
|
|
2648
|
+
if (col) {
|
|
2649
|
+
const sortKey = COLUMN_SORT_MAP[col.name]
|
|
2650
|
+
if (sortKey) {
|
|
2651
|
+
setSortColumnFromClick(sortKey)
|
|
2652
|
+
persistUiSettings()
|
|
2653
|
+
} else if (col.name === 'tier') {
|
|
2654
|
+
// 📖 Clicking the Tier header cycles the tier filter (same as T key)
|
|
2655
|
+
state.tierFilterMode = (state.tierFilterMode + 1) % TIER_CYCLE.length
|
|
2656
|
+
applyTierFilter()
|
|
2657
|
+
const visible = state.results.filter(r => !r.hidden)
|
|
2658
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
2659
|
+
pinFavorites: state.favoritesPinnedAndSticky,
|
|
2660
|
+
})
|
|
2661
|
+
state.cursor = 0
|
|
2662
|
+
state.scrollOffset = 0
|
|
2663
|
+
persistUiSettings()
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
return
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
// 📖 Check if click is on a model row → move cursor (or select on double-click)
|
|
2670
|
+
// 📖 Right-click toggles favorite on that row (same as F key)
|
|
2671
|
+
if (y >= layout.firstModelRow && y <= layout.lastModelRow) {
|
|
2672
|
+
const rowOffset = y - layout.firstModelRow
|
|
2673
|
+
const modelIdx = layout.viewportStartIdx + rowOffset
|
|
2674
|
+
if (modelIdx >= layout.viewportStartIdx && modelIdx < layout.viewportEndIdx) {
|
|
2675
|
+
state.cursor = modelIdx
|
|
2676
|
+
adjustScrollOffset(state)
|
|
2677
|
+
|
|
2678
|
+
if (evt.button === 'right') {
|
|
2679
|
+
// 📖 Right-click: toggle favorite on this model row
|
|
2680
|
+
toggleFavoriteAtRow(modelIdx)
|
|
2681
|
+
} else if (evt.type === 'double-click') {
|
|
2682
|
+
// 📖 Double-click triggers the Enter action (select model).
|
|
2683
|
+
process.stdin.emit('keypress', '\r', { name: 'return', ctrl: false, meta: false, shift: false })
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
return
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// ── Footer hotkey click zones ──────────────────────────────────────
|
|
2690
|
+
// 📖 Check if click lands on a footer hotkey zone and emit the corresponding keypress.
|
|
2691
|
+
if (layout.footerHotkeys && layout.footerHotkeys.length > 0) {
|
|
2692
|
+
const zone = layout.footerHotkeys.find(z => y === z.row && x >= z.xStart && x <= z.xEnd)
|
|
2693
|
+
if (zone) {
|
|
2694
|
+
// 📖 Map the footer zone key to a synthetic keypress.
|
|
2695
|
+
// 📖 Most are single-character keys; special cases like ctrl+p need special handling.
|
|
2696
|
+
if (zone.key === 'ctrl+p') {
|
|
2697
|
+
process.stdin.emit('keypress', '\x10', { name: 'p', ctrl: true, meta: false, shift: false })
|
|
2698
|
+
} else {
|
|
2699
|
+
process.stdin.emit('keypress', zone.key, { name: zone.key, ctrl: false, meta: false, shift: false })
|
|
2700
|
+
}
|
|
2701
|
+
return
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// 📖 Clicks outside any recognized zone are silently ignored.
|
|
2706
|
+
}
|
|
2707
|
+
}
|