free-coding-models 0.3.17 → 0.3.19
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 +26 -0
- package/README.md +11 -1
- package/package.json +1 -1
- package/src/app.js +106 -3
- package/src/command-palette.js +170 -0
- package/src/config.js +3 -3
- package/src/key-handler.js +492 -142
- package/src/openclaw.js +39 -5
- package/src/opencode.js +2 -1
- package/src/overlays.js +426 -208
- package/src/render-helpers.js +1 -1
- package/src/render-table.js +141 -177
- package/src/theme.js +294 -43
- package/src/tier-colors.js +15 -17
- package/src/tool-bootstrap.js +310 -0
- package/src/tool-launchers.js +12 -7
- package/src/ui-config.js +24 -31
package/src/key-handler.js
CHANGED
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
import { loadChangelog } from './changelog-loader.js'
|
|
31
31
|
import { loadConfig, replaceConfigContents } from './config.js'
|
|
32
32
|
import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
|
|
33
|
+
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
34
|
+
import { buildCommandPaletteEntries, filterCommandPaletteEntries } from './command-palette.js'
|
|
33
35
|
|
|
34
36
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
35
37
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
@@ -184,6 +186,9 @@ export function createKeyHandler(ctx) {
|
|
|
184
186
|
startOpenCode,
|
|
185
187
|
startExternalTool,
|
|
186
188
|
getToolModeOrder,
|
|
189
|
+
getToolInstallPlan,
|
|
190
|
+
isToolInstalled,
|
|
191
|
+
installToolWithPlan,
|
|
187
192
|
startRecommendAnalysis,
|
|
188
193
|
stopRecommendAnalysis,
|
|
189
194
|
sendBugReport,
|
|
@@ -207,6 +212,97 @@ export function createKeyHandler(ctx) {
|
|
|
207
212
|
|
|
208
213
|
let userSelected = null
|
|
209
214
|
|
|
215
|
+
function resetToolInstallPrompt() {
|
|
216
|
+
state.toolInstallPromptOpen = false
|
|
217
|
+
state.toolInstallPromptCursor = 0
|
|
218
|
+
state.toolInstallPromptScrollOffset = 0
|
|
219
|
+
state.toolInstallPromptMode = null
|
|
220
|
+
state.toolInstallPromptModel = null
|
|
221
|
+
state.toolInstallPromptPlan = null
|
|
222
|
+
state.toolInstallPromptErrorMsg = null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function shouldCheckMissingTool(mode) {
|
|
226
|
+
return mode !== 'opencode-desktop'
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function launchSelectedModel(selected, options = {}) {
|
|
230
|
+
const { uiAlreadyStopped = false } = options
|
|
231
|
+
userSelected = { modelId: selected.modelId, label: selected.label, tier: selected.tier, providerKey: selected.providerKey }
|
|
232
|
+
|
|
233
|
+
if (!uiAlreadyStopped) {
|
|
234
|
+
readline.emitKeypressEvents(process.stdin)
|
|
235
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
236
|
+
stopUi()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 📖 Show selection status before handing control to the target tool.
|
|
240
|
+
if (selected.status === 'timeout') {
|
|
241
|
+
console.log(chalk.yellow(` ⚠ Selected: ${selected.label} (currently timing out)`))
|
|
242
|
+
} else if (selected.status === 'down') {
|
|
243
|
+
console.log(chalk.red(` ⚠ Selected: ${selected.label} (currently down)`))
|
|
244
|
+
} else {
|
|
245
|
+
console.log(chalk.cyan(` ✓ Selected: ${selected.label}`))
|
|
246
|
+
}
|
|
247
|
+
console.log()
|
|
248
|
+
|
|
249
|
+
// 📖 OpenClaw manages API keys inside its own config file. All other tools
|
|
250
|
+
// 📖 still need a provider key to be useful, so keep the existing warning.
|
|
251
|
+
if (state.mode !== 'openclaw') {
|
|
252
|
+
const selectedApiKey = getApiKey(state.config, selected.providerKey)
|
|
253
|
+
if (!selectedApiKey) {
|
|
254
|
+
console.log(chalk.yellow(` Warning: No API key configured for ${selected.providerKey}.`))
|
|
255
|
+
console.log(chalk.yellow(` The selected tool may not be able to use ${selected.label}.`))
|
|
256
|
+
console.log(chalk.dim(` Set ${ENV_VAR_NAMES[selected.providerKey] || selected.providerKey.toUpperCase() + '_API_KEY'} or configure via settings (P key).`))
|
|
257
|
+
console.log()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let exitCode = 0
|
|
262
|
+
if (state.mode === 'openclaw') {
|
|
263
|
+
exitCode = await startOpenClaw(userSelected, state.config, { launchCli: true })
|
|
264
|
+
} else if (state.mode === 'opencode-desktop') {
|
|
265
|
+
exitCode = await startOpenCodeDesktop(userSelected, state.config)
|
|
266
|
+
} else if (state.mode === 'opencode') {
|
|
267
|
+
exitCode = await startOpenCode(userSelected, state.config)
|
|
268
|
+
} else {
|
|
269
|
+
exitCode = await startExternalTool(state.mode, userSelected, state.config)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
process.exit(typeof exitCode === 'number' ? exitCode : 0)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function installMissingToolAndLaunch(selected, installPlan) {
|
|
276
|
+
const currentPlan = installPlan || getToolInstallPlan(state.mode)
|
|
277
|
+
stopUi({ resetRawMode: true })
|
|
278
|
+
|
|
279
|
+
console.log(chalk.cyan(` 📦 Installing missing tool for ${state.mode}...`))
|
|
280
|
+
if (currentPlan?.summary) console.log(chalk.dim(` ${currentPlan.summary}`))
|
|
281
|
+
if (currentPlan?.shellCommand) console.log(chalk.dim(` ${currentPlan.shellCommand}`))
|
|
282
|
+
if (currentPlan?.note) console.log(chalk.dim(` ${currentPlan.note}`))
|
|
283
|
+
console.log()
|
|
284
|
+
|
|
285
|
+
const installResult = await installToolWithPlan(currentPlan)
|
|
286
|
+
if (!installResult.ok) {
|
|
287
|
+
console.log(chalk.red(` X Tool installation failed with exit code ${installResult.exitCode}.`))
|
|
288
|
+
if (currentPlan?.docsUrl) console.log(chalk.dim(` Docs: ${currentPlan.docsUrl}`))
|
|
289
|
+
console.log()
|
|
290
|
+
process.exit(installResult.exitCode || 1)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (shouldCheckMissingTool(state.mode) && !isToolInstalled(state.mode)) {
|
|
294
|
+
console.log(chalk.yellow(' ⚠ The installer finished, but the tool is still not reachable from this terminal session.'))
|
|
295
|
+
console.log(chalk.dim(' Restart your shell or add the tool bin directory to PATH, then retry the launch.'))
|
|
296
|
+
if (currentPlan?.docsUrl) console.log(chalk.dim(` Docs: ${currentPlan.docsUrl}`))
|
|
297
|
+
console.log()
|
|
298
|
+
process.exit(1)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log(chalk.green(' ✓ Tool installed successfully. Continuing with the selected model...'))
|
|
302
|
+
console.log()
|
|
303
|
+
await launchSelectedModel(selected, { uiAlreadyStopped: true })
|
|
304
|
+
}
|
|
305
|
+
|
|
210
306
|
// ─── Settings key test helper ───────────────────────────────────────────────
|
|
211
307
|
// 📖 Fires a single ping to the selected provider to verify the API key works.
|
|
212
308
|
async function testProviderKey(providerKey) {
|
|
@@ -383,6 +479,20 @@ export function createKeyHandler(ctx) {
|
|
|
383
479
|
saveConfig(state.config)
|
|
384
480
|
}
|
|
385
481
|
|
|
482
|
+
// 📖 Theme switches need to update both persisted preference and the live
|
|
483
|
+
// 📖 semantic palette immediately so every screen redraw adopts the new colors.
|
|
484
|
+
function applyThemeSetting(nextTheme) {
|
|
485
|
+
if (!state.config.settings) state.config.settings = {}
|
|
486
|
+
state.config.settings.theme = nextTheme
|
|
487
|
+
saveConfig(state.config)
|
|
488
|
+
detectActiveTheme(nextTheme)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function cycleGlobalTheme() {
|
|
492
|
+
const currentTheme = state.config.settings?.theme || 'auto'
|
|
493
|
+
applyThemeSetting(cycleThemeSetting(currentTheme))
|
|
494
|
+
}
|
|
495
|
+
|
|
386
496
|
function resetInstallEndpointsOverlay() {
|
|
387
497
|
state.installEndpointsOpen = false
|
|
388
498
|
state.installEndpointsPhase = 'providers'
|
|
@@ -427,10 +537,320 @@ export function createKeyHandler(ctx) {
|
|
|
427
537
|
state.installEndpointsErrorMsg = null
|
|
428
538
|
}
|
|
429
539
|
|
|
540
|
+
// 📖 Persist current table-view preferences so sort/filter state survives restarts.
|
|
541
|
+
function persistUiSettings() {
|
|
542
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
543
|
+
state.config.settings.tierFilter = TIER_CYCLE[state.tierFilterMode]
|
|
544
|
+
state.config.settings.originFilter = ORIGIN_CYCLE[state.originFilterMode] ?? null
|
|
545
|
+
state.config.settings.sortColumn = state.sortColumn
|
|
546
|
+
state.config.settings.sortAsc = state.sortDirection === 'asc'
|
|
547
|
+
saveConfig(state.config)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 📖 Shared table refresh helper so command-palette and hotkeys keep identical behavior.
|
|
551
|
+
function refreshVisibleSorted({ resetCursor = true } = {}) {
|
|
552
|
+
const visible = state.results.filter(r => !r.hidden)
|
|
553
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
554
|
+
if (resetCursor) {
|
|
555
|
+
state.cursor = 0
|
|
556
|
+
state.scrollOffset = 0
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
if (state.cursor >= state.visibleSorted.length) state.cursor = Math.max(0, state.visibleSorted.length - 1)
|
|
560
|
+
adjustScrollOffset(state)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function setSortColumnFromCommand(col) {
|
|
564
|
+
if (state.sortColumn === col) {
|
|
565
|
+
state.sortDirection = state.sortDirection === 'asc' ? 'desc' : 'asc'
|
|
566
|
+
} else {
|
|
567
|
+
state.sortColumn = col
|
|
568
|
+
state.sortDirection = 'asc'
|
|
569
|
+
}
|
|
570
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
571
|
+
persistUiSettings()
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function setTierFilterFromCommand(tierLabel) {
|
|
575
|
+
const nextMode = tierLabel === null ? 0 : TIER_CYCLE.indexOf(tierLabel)
|
|
576
|
+
state.tierFilterMode = nextMode >= 0 ? nextMode : 0
|
|
577
|
+
applyTierFilter()
|
|
578
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
579
|
+
persistUiSettings()
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function openSettingsOverlay() {
|
|
583
|
+
state.settingsOpen = true
|
|
584
|
+
state.settingsCursor = 0
|
|
585
|
+
state.settingsEditMode = false
|
|
586
|
+
state.settingsAddKeyMode = false
|
|
587
|
+
state.settingsEditBuffer = ''
|
|
588
|
+
state.settingsScrollOffset = 0
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function openRecommendOverlay() {
|
|
592
|
+
state.recommendOpen = true
|
|
593
|
+
state.recommendPhase = 'questionnaire'
|
|
594
|
+
state.recommendQuestion = 0
|
|
595
|
+
state.recommendCursor = 0
|
|
596
|
+
state.recommendAnswers = { taskType: null, priority: null, contextBudget: null }
|
|
597
|
+
state.recommendResults = []
|
|
598
|
+
state.recommendScrollOffset = 0
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function openInstallEndpointsOverlay() {
|
|
602
|
+
state.installEndpointsOpen = true
|
|
603
|
+
state.installEndpointsPhase = 'providers'
|
|
604
|
+
state.installEndpointsCursor = 0
|
|
605
|
+
state.installEndpointsScrollOffset = 0
|
|
606
|
+
state.installEndpointsProviderKey = null
|
|
607
|
+
state.installEndpointsToolMode = null
|
|
608
|
+
state.installEndpointsConnectionMode = null
|
|
609
|
+
state.installEndpointsScope = null
|
|
610
|
+
state.installEndpointsSelectedModelIds = new Set()
|
|
611
|
+
state.installEndpointsErrorMsg = null
|
|
612
|
+
state.installEndpointsResult = null
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function openFeedbackOverlay() {
|
|
616
|
+
state.feedbackOpen = true
|
|
617
|
+
state.bugReportBuffer = ''
|
|
618
|
+
state.bugReportStatus = 'idle'
|
|
619
|
+
state.bugReportError = null
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function openChangelogOverlay() {
|
|
623
|
+
state.changelogOpen = true
|
|
624
|
+
state.changelogScrollOffset = 0
|
|
625
|
+
state.changelogPhase = 'index'
|
|
626
|
+
state.changelogCursor = 0
|
|
627
|
+
state.changelogSelectedVersion = null
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function cycleToolMode() {
|
|
631
|
+
const modeOrder = getToolModeOrder()
|
|
632
|
+
const currentIndex = modeOrder.indexOf(state.mode)
|
|
633
|
+
const nextIndex = (currentIndex + 1) % modeOrder.length
|
|
634
|
+
state.mode = modeOrder[nextIndex]
|
|
635
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
636
|
+
state.config.settings.preferredToolMode = state.mode
|
|
637
|
+
saveConfig(state.config)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function resetViewSettings() {
|
|
641
|
+
state.tierFilterMode = 0
|
|
642
|
+
state.originFilterMode = 0
|
|
643
|
+
state.sortColumn = 'avg'
|
|
644
|
+
state.sortDirection = 'asc'
|
|
645
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
646
|
+
delete state.config.settings.tierFilter
|
|
647
|
+
delete state.config.settings.originFilter
|
|
648
|
+
delete state.config.settings.sortColumn
|
|
649
|
+
delete state.config.settings.sortAsc
|
|
650
|
+
saveConfig(state.config)
|
|
651
|
+
applyTierFilter()
|
|
652
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function toggleFavoriteOnSelectedRow() {
|
|
656
|
+
const selected = state.visibleSorted[state.cursor]
|
|
657
|
+
if (!selected) return
|
|
658
|
+
const wasFavorite = selected.isFavorite
|
|
659
|
+
toggleFavoriteModel(state.config, selected.providerKey, selected.modelId)
|
|
660
|
+
syncFavoriteFlags(state.results, state.config)
|
|
661
|
+
applyTierFilter()
|
|
662
|
+
refreshVisibleSorted({ resetCursor: false })
|
|
663
|
+
|
|
664
|
+
if (wasFavorite) {
|
|
665
|
+
state.cursor = 0
|
|
666
|
+
state.scrollOffset = 0
|
|
667
|
+
return
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const selectedKey = toFavoriteKey(selected.providerKey, selected.modelId)
|
|
671
|
+
const newCursor = state.visibleSorted.findIndex(r => toFavoriteKey(r.providerKey, r.modelId) === selectedKey)
|
|
672
|
+
if (newCursor >= 0) state.cursor = newCursor
|
|
673
|
+
adjustScrollOffset(state)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function commandPaletteHasBlockingOverlay() {
|
|
677
|
+
return state.settingsOpen
|
|
678
|
+
|| state.installEndpointsOpen
|
|
679
|
+
|| state.toolInstallPromptOpen
|
|
680
|
+
|| state.recommendOpen
|
|
681
|
+
|| state.feedbackOpen
|
|
682
|
+
|| state.helpVisible
|
|
683
|
+
|| state.changelogOpen
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function refreshCommandPaletteResults() {
|
|
687
|
+
const commands = buildCommandPaletteEntries()
|
|
688
|
+
state.commandPaletteResults = filterCommandPaletteEntries(commands, state.commandPaletteQuery)
|
|
689
|
+
if (state.commandPaletteCursor >= state.commandPaletteResults.length) {
|
|
690
|
+
state.commandPaletteCursor = Math.max(0, state.commandPaletteResults.length - 1)
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function openCommandPalette() {
|
|
695
|
+
state.commandPaletteOpen = true
|
|
696
|
+
state.commandPaletteFrozenTable = null
|
|
697
|
+
state.commandPaletteQuery = ''
|
|
698
|
+
state.commandPaletteCursor = 0
|
|
699
|
+
state.commandPaletteScrollOffset = 0
|
|
700
|
+
refreshCommandPaletteResults()
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function closeCommandPalette() {
|
|
704
|
+
state.commandPaletteOpen = false
|
|
705
|
+
state.commandPaletteFrozenTable = null
|
|
706
|
+
state.commandPaletteQuery = ''
|
|
707
|
+
state.commandPaletteCursor = 0
|
|
708
|
+
state.commandPaletteScrollOffset = 0
|
|
709
|
+
state.commandPaletteResults = []
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function executeCommandPaletteEntry(entry) {
|
|
713
|
+
if (!entry?.id) return
|
|
714
|
+
|
|
715
|
+
if (entry.id.startsWith('filter-tier-')) {
|
|
716
|
+
setTierFilterFromCommand(entry.tierValue ?? null)
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
switch (entry.id) {
|
|
721
|
+
case 'filter-provider-cycle':
|
|
722
|
+
state.originFilterMode = (state.originFilterMode + 1) % ORIGIN_CYCLE.length
|
|
723
|
+
applyTierFilter()
|
|
724
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
725
|
+
persistUiSettings()
|
|
726
|
+
return
|
|
727
|
+
case 'filter-configured-toggle':
|
|
728
|
+
state.hideUnconfiguredModels = !state.hideUnconfiguredModels
|
|
729
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
730
|
+
state.config.settings.hideUnconfiguredModels = state.hideUnconfiguredModels
|
|
731
|
+
saveConfig(state.config)
|
|
732
|
+
applyTierFilter()
|
|
733
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
734
|
+
return
|
|
735
|
+
case 'sort-rank': return setSortColumnFromCommand('rank')
|
|
736
|
+
case 'sort-tier': return setSortColumnFromCommand('tier')
|
|
737
|
+
case 'sort-provider': return setSortColumnFromCommand('origin')
|
|
738
|
+
case 'sort-model': return setSortColumnFromCommand('model')
|
|
739
|
+
case 'sort-latest-ping': return setSortColumnFromCommand('ping')
|
|
740
|
+
case 'sort-avg-ping': return setSortColumnFromCommand('avg')
|
|
741
|
+
case 'sort-swe': return setSortColumnFromCommand('swe')
|
|
742
|
+
case 'sort-ctx': return setSortColumnFromCommand('ctx')
|
|
743
|
+
case 'sort-health': return setSortColumnFromCommand('condition')
|
|
744
|
+
case 'sort-verdict': return setSortColumnFromCommand('verdict')
|
|
745
|
+
case 'sort-stability': return setSortColumnFromCommand('stability')
|
|
746
|
+
case 'sort-uptime': return setSortColumnFromCommand('uptime')
|
|
747
|
+
case 'open-settings': return openSettingsOverlay()
|
|
748
|
+
case 'open-help':
|
|
749
|
+
state.helpVisible = true
|
|
750
|
+
state.helpScrollOffset = 0
|
|
751
|
+
return
|
|
752
|
+
case 'open-changelog': return openChangelogOverlay()
|
|
753
|
+
case 'open-feedback': return openFeedbackOverlay()
|
|
754
|
+
case 'open-recommend': return openRecommendOverlay()
|
|
755
|
+
case 'open-install-endpoints': return openInstallEndpointsOverlay()
|
|
756
|
+
case 'action-cycle-theme': return cycleGlobalTheme()
|
|
757
|
+
case 'action-cycle-tool-mode': return cycleToolMode()
|
|
758
|
+
case 'action-cycle-ping-mode': {
|
|
759
|
+
const currentIdx = PING_MODE_CYCLE.indexOf(state.pingMode)
|
|
760
|
+
const nextIdx = currentIdx >= 0 ? (currentIdx + 1) % PING_MODE_CYCLE.length : 0
|
|
761
|
+
setPingMode(PING_MODE_CYCLE[nextIdx], 'manual')
|
|
762
|
+
return
|
|
763
|
+
}
|
|
764
|
+
case 'action-toggle-favorite': return toggleFavoriteOnSelectedRow()
|
|
765
|
+
case 'action-reset-view': return resetViewSettings()
|
|
766
|
+
default:
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
430
771
|
return async (str, key) => {
|
|
431
772
|
if (!key) return
|
|
432
773
|
noteUserActivity()
|
|
433
774
|
|
|
775
|
+
// 📖 Ctrl+P toggles the command palette from the main table only.
|
|
776
|
+
if (key.ctrl && key.name === 'p') {
|
|
777
|
+
if (state.commandPaletteOpen) {
|
|
778
|
+
closeCommandPalette()
|
|
779
|
+
return
|
|
780
|
+
}
|
|
781
|
+
if (!commandPaletteHasBlockingOverlay()) {
|
|
782
|
+
openCommandPalette()
|
|
783
|
+
}
|
|
784
|
+
return
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// 📖 Command palette captures the keyboard while active.
|
|
788
|
+
if (state.commandPaletteOpen) {
|
|
789
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
790
|
+
|
|
791
|
+
const pageStep = Math.max(1, (state.terminalRows || 1) - 10)
|
|
792
|
+
|
|
793
|
+
if (key.name === 'escape') {
|
|
794
|
+
closeCommandPalette()
|
|
795
|
+
return
|
|
796
|
+
}
|
|
797
|
+
if (key.name === 'up') {
|
|
798
|
+
const count = state.commandPaletteResults.length
|
|
799
|
+
if (count === 0) return
|
|
800
|
+
state.commandPaletteCursor = state.commandPaletteCursor > 0 ? state.commandPaletteCursor - 1 : count - 1
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
if (key.name === 'down') {
|
|
804
|
+
const count = state.commandPaletteResults.length
|
|
805
|
+
if (count === 0) return
|
|
806
|
+
state.commandPaletteCursor = state.commandPaletteCursor < count - 1 ? state.commandPaletteCursor + 1 : 0
|
|
807
|
+
return
|
|
808
|
+
}
|
|
809
|
+
if (key.name === 'pageup') {
|
|
810
|
+
state.commandPaletteCursor = Math.max(0, state.commandPaletteCursor - pageStep)
|
|
811
|
+
return
|
|
812
|
+
}
|
|
813
|
+
if (key.name === 'pagedown') {
|
|
814
|
+
const max = Math.max(0, state.commandPaletteResults.length - 1)
|
|
815
|
+
state.commandPaletteCursor = Math.min(max, state.commandPaletteCursor + pageStep)
|
|
816
|
+
return
|
|
817
|
+
}
|
|
818
|
+
if (key.name === 'home') {
|
|
819
|
+
state.commandPaletteCursor = 0
|
|
820
|
+
return
|
|
821
|
+
}
|
|
822
|
+
if (key.name === 'end') {
|
|
823
|
+
state.commandPaletteCursor = Math.max(0, state.commandPaletteResults.length - 1)
|
|
824
|
+
return
|
|
825
|
+
}
|
|
826
|
+
if (key.name === 'backspace') {
|
|
827
|
+
state.commandPaletteQuery = state.commandPaletteQuery.slice(0, -1)
|
|
828
|
+
state.commandPaletteCursor = 0
|
|
829
|
+
state.commandPaletteScrollOffset = 0
|
|
830
|
+
refreshCommandPaletteResults()
|
|
831
|
+
return
|
|
832
|
+
}
|
|
833
|
+
if (key.name === 'return') {
|
|
834
|
+
const selectedCommand = state.commandPaletteResults[state.commandPaletteCursor]
|
|
835
|
+
closeCommandPalette()
|
|
836
|
+
executeCommandPaletteEntry(selectedCommand)
|
|
837
|
+
return
|
|
838
|
+
}
|
|
839
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
840
|
+
state.commandPaletteQuery += str
|
|
841
|
+
state.commandPaletteCursor = 0
|
|
842
|
+
state.commandPaletteScrollOffset = 0
|
|
843
|
+
refreshCommandPaletteResults()
|
|
844
|
+
return
|
|
845
|
+
}
|
|
846
|
+
return
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (!state.feedbackOpen && !state.settingsEditMode && !state.settingsAddKeyMode && key.name === 'g' && !key.ctrl && !key.meta) {
|
|
850
|
+
cycleGlobalTheme()
|
|
851
|
+
return
|
|
852
|
+
}
|
|
853
|
+
|
|
434
854
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
435
855
|
|
|
436
856
|
// 📖 Install Endpoints overlay: provider → tool → connection → scope → optional model subset.
|
|
@@ -614,6 +1034,44 @@ export function createKeyHandler(ctx) {
|
|
|
614
1034
|
return
|
|
615
1035
|
}
|
|
616
1036
|
|
|
1037
|
+
if (state.toolInstallPromptOpen) {
|
|
1038
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1039
|
+
|
|
1040
|
+
const installPlan = state.toolInstallPromptPlan || getToolInstallPlan(state.toolInstallPromptMode)
|
|
1041
|
+
const installSupported = Boolean(installPlan?.supported)
|
|
1042
|
+
|
|
1043
|
+
if (key.name === 'escape') {
|
|
1044
|
+
resetToolInstallPrompt()
|
|
1045
|
+
return
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (installSupported && key.name === 'up') {
|
|
1049
|
+
state.toolInstallPromptCursor = Math.max(0, state.toolInstallPromptCursor - 1)
|
|
1050
|
+
return
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (installSupported && key.name === 'down') {
|
|
1054
|
+
state.toolInstallPromptCursor = Math.min(1, state.toolInstallPromptCursor + 1)
|
|
1055
|
+
return
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (key.name === 'return') {
|
|
1059
|
+
if (!installSupported) {
|
|
1060
|
+
resetToolInstallPrompt()
|
|
1061
|
+
return
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const selectedModel = state.toolInstallPromptModel
|
|
1065
|
+
const shouldInstall = state.toolInstallPromptCursor === 0
|
|
1066
|
+
resetToolInstallPrompt()
|
|
1067
|
+
|
|
1068
|
+
if (!shouldInstall || !selectedModel) return
|
|
1069
|
+
await installMissingToolAndLaunch(selectedModel, installPlan)
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
return
|
|
1073
|
+
}
|
|
1074
|
+
|
|
617
1075
|
// 📖 Feedback overlay: intercept ALL keys while overlay is active.
|
|
618
1076
|
// 📖 Enter → send to Discord, Esc → cancel, Backspace → delete char, printable → append to buffer.
|
|
619
1077
|
if (state.feedbackOpen) {
|
|
@@ -1078,6 +1536,11 @@ export function createKeyHandler(ctx) {
|
|
|
1078
1536
|
return
|
|
1079
1537
|
}
|
|
1080
1538
|
|
|
1539
|
+
if (state.settingsCursor === themeRowIdx) {
|
|
1540
|
+
cycleGlobalTheme()
|
|
1541
|
+
return
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1081
1544
|
if (state.settingsCursor === cleanupLegacyProxyRowIdx) {
|
|
1082
1545
|
runLegacyProxyCleanup()
|
|
1083
1546
|
return
|
|
@@ -1098,6 +1561,7 @@ export function createKeyHandler(ctx) {
|
|
|
1098
1561
|
|
|
1099
1562
|
// 📖 Enter edit mode for the selected provider's key
|
|
1100
1563
|
const pk = providerKeys[state.settingsCursor]
|
|
1564
|
+
if (!pk) return
|
|
1101
1565
|
state.settingsEditBuffer = resolveApiKeys(state.config, pk)[0] ?? ''
|
|
1102
1566
|
state.settingsEditMode = true
|
|
1103
1567
|
return
|
|
@@ -1112,15 +1576,7 @@ export function createKeyHandler(ctx) {
|
|
|
1112
1576
|
) return
|
|
1113
1577
|
// 📖 Theme configuration cycle inside settings
|
|
1114
1578
|
if (state.settingsCursor === themeRowIdx) {
|
|
1115
|
-
|
|
1116
|
-
const currentTheme = state.config.settings?.theme || 'dark'
|
|
1117
|
-
const nextIndex = (themes.indexOf(currentTheme) + 1) % themes.length
|
|
1118
|
-
state.config.settings.theme = themes[nextIndex]
|
|
1119
|
-
saveConfig(state.config)
|
|
1120
|
-
try {
|
|
1121
|
-
const { detectActiveTheme } = await import('../src/theme.js')
|
|
1122
|
-
detectActiveTheme(state.config.settings.theme)
|
|
1123
|
-
} catch {}
|
|
1579
|
+
cycleGlobalTheme()
|
|
1124
1580
|
return
|
|
1125
1581
|
}
|
|
1126
1582
|
// 📖 Widths Warning toggle (disable/enable)
|
|
@@ -1142,6 +1598,8 @@ export function createKeyHandler(ctx) {
|
|
|
1142
1598
|
if (key.name === 't') {
|
|
1143
1599
|
if (
|
|
1144
1600
|
state.settingsCursor === updateRowIdx
|
|
1601
|
+
|| state.settingsCursor === widthWarningRowIdx
|
|
1602
|
+
|| state.settingsCursor === themeRowIdx
|
|
1145
1603
|
|| state.settingsCursor === cleanupLegacyProxyRowIdx
|
|
1146
1604
|
|| state.settingsCursor === changelogViewRowIdx
|
|
1147
1605
|
) return
|
|
@@ -1149,6 +1607,7 @@ export function createKeyHandler(ctx) {
|
|
|
1149
1607
|
|
|
1150
1608
|
// 📖 Test the selected provider's key (fires a real ping)
|
|
1151
1609
|
const pk = providerKeys[state.settingsCursor]
|
|
1610
|
+
if (!pk) return
|
|
1152
1611
|
testProviderKey(pk)
|
|
1153
1612
|
return
|
|
1154
1613
|
}
|
|
@@ -1193,41 +1652,20 @@ export function createKeyHandler(ctx) {
|
|
|
1193
1652
|
}
|
|
1194
1653
|
|
|
1195
1654
|
// 📖 P key: open settings screen
|
|
1196
|
-
if (key.name === 'p' && !key.shift) {
|
|
1197
|
-
|
|
1198
|
-
state.settingsCursor = 0
|
|
1199
|
-
state.settingsEditMode = false
|
|
1200
|
-
state.settingsAddKeyMode = false
|
|
1201
|
-
state.settingsEditBuffer = ''
|
|
1202
|
-
state.settingsScrollOffset = 0
|
|
1655
|
+
if (key.name === 'p' && !key.shift && !key.ctrl && !key.meta) {
|
|
1656
|
+
openSettingsOverlay()
|
|
1203
1657
|
return
|
|
1204
1658
|
}
|
|
1205
1659
|
|
|
1206
1660
|
// 📖 Q key: open Smart Recommend overlay
|
|
1207
1661
|
if (key.name === 'q') {
|
|
1208
|
-
|
|
1209
|
-
state.recommendPhase = 'questionnaire'
|
|
1210
|
-
state.recommendQuestion = 0
|
|
1211
|
-
state.recommendCursor = 0
|
|
1212
|
-
state.recommendAnswers = { taskType: null, priority: null, contextBudget: null }
|
|
1213
|
-
state.recommendResults = []
|
|
1214
|
-
state.recommendScrollOffset = 0
|
|
1662
|
+
openRecommendOverlay()
|
|
1215
1663
|
return
|
|
1216
1664
|
}
|
|
1217
1665
|
|
|
1218
1666
|
// 📖 Y key: open Install Endpoints flow for configured providers.
|
|
1219
1667
|
if (key.name === 'y') {
|
|
1220
|
-
|
|
1221
|
-
state.installEndpointsPhase = 'providers'
|
|
1222
|
-
state.installEndpointsCursor = 0
|
|
1223
|
-
state.installEndpointsScrollOffset = 0
|
|
1224
|
-
state.installEndpointsProviderKey = null
|
|
1225
|
-
state.installEndpointsToolMode = null
|
|
1226
|
-
state.installEndpointsConnectionMode = null
|
|
1227
|
-
state.installEndpointsScope = null
|
|
1228
|
-
state.installEndpointsSelectedModelIds = new Set()
|
|
1229
|
-
state.installEndpointsErrorMsg = null
|
|
1230
|
-
state.installEndpointsResult = null
|
|
1668
|
+
openInstallEndpointsOverlay()
|
|
1231
1669
|
return
|
|
1232
1670
|
}
|
|
1233
1671
|
|
|
@@ -1235,34 +1673,9 @@ export function createKeyHandler(ctx) {
|
|
|
1235
1673
|
|
|
1236
1674
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1237
1675
|
|
|
1238
|
-
// 📖 Helper: persist current UI view settings (tier, provider, sort) to config.settings
|
|
1239
|
-
// 📖 Called after every T / D / sort key so preferences survive session restarts.
|
|
1240
|
-
function persistUiSettings() {
|
|
1241
|
-
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
1242
|
-
state.config.settings.tierFilter = TIER_CYCLE[state.tierFilterMode]
|
|
1243
|
-
state.config.settings.originFilter = ORIGIN_CYCLE[state.originFilterMode] ?? null
|
|
1244
|
-
state.config.settings.sortColumn = state.sortColumn
|
|
1245
|
-
state.config.settings.sortAsc = state.sortDirection === 'asc'
|
|
1246
|
-
saveConfig(state.config)
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
1676
|
// 📖 Shift+R: reset all UI view settings to defaults (tier, sort, provider) and clear persisted config
|
|
1250
1677
|
if (key.name === 'r' && key.shift) {
|
|
1251
|
-
|
|
1252
|
-
state.originFilterMode = 0
|
|
1253
|
-
state.sortColumn = 'avg'
|
|
1254
|
-
state.sortDirection = 'asc'
|
|
1255
|
-
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
1256
|
-
delete state.config.settings.tierFilter
|
|
1257
|
-
delete state.config.settings.originFilter
|
|
1258
|
-
delete state.config.settings.sortColumn
|
|
1259
|
-
delete state.config.settings.sortAsc
|
|
1260
|
-
saveConfig(state.config)
|
|
1261
|
-
applyTierFilter()
|
|
1262
|
-
const visible = state.results.filter(r => !r.hidden)
|
|
1263
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1264
|
-
state.cursor = 0
|
|
1265
|
-
state.scrollOffset = 0
|
|
1678
|
+
resetViewSettings()
|
|
1266
1679
|
return
|
|
1267
1680
|
}
|
|
1268
1681
|
|
|
@@ -1277,54 +1690,19 @@ export function createKeyHandler(ctx) {
|
|
|
1277
1690
|
|
|
1278
1691
|
if (sortKeys[key.name] && !key.ctrl && !key.shift) {
|
|
1279
1692
|
const col = sortKeys[key.name]
|
|
1280
|
-
|
|
1281
|
-
if (state.sortColumn === col) {
|
|
1282
|
-
state.sortDirection = state.sortDirection === 'asc' ? 'desc' : 'asc'
|
|
1283
|
-
} else {
|
|
1284
|
-
state.sortColumn = col
|
|
1285
|
-
state.sortDirection = 'asc'
|
|
1286
|
-
}
|
|
1287
|
-
// 📖 Recompute visible sorted list and reset cursor to top to avoid stale index
|
|
1288
|
-
const visible = state.results.filter(r => !r.hidden)
|
|
1289
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1290
|
-
state.cursor = 0
|
|
1291
|
-
state.scrollOffset = 0
|
|
1292
|
-
persistUiSettings()
|
|
1693
|
+
setSortColumnFromCommand(col)
|
|
1293
1694
|
return
|
|
1294
1695
|
}
|
|
1295
1696
|
|
|
1296
1697
|
// 📖 F key: toggle favorite on the currently selected row and persist to config.
|
|
1297
1698
|
if (key.name === 'f') {
|
|
1298
|
-
|
|
1299
|
-
if (!selected) return
|
|
1300
|
-
const wasFavorite = selected.isFavorite
|
|
1301
|
-
toggleFavoriteModel(state.config, selected.providerKey, selected.modelId)
|
|
1302
|
-
syncFavoriteFlags(state.results, state.config)
|
|
1303
|
-
applyTierFilter()
|
|
1304
|
-
const visible = state.results.filter(r => !r.hidden)
|
|
1305
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1306
|
-
|
|
1307
|
-
// 📖 UX rule: when unpinning a favorite, jump back to the top of the list.
|
|
1308
|
-
if (wasFavorite) {
|
|
1309
|
-
state.cursor = 0
|
|
1310
|
-
state.scrollOffset = 0
|
|
1311
|
-
return
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
const selectedKey = toFavoriteKey(selected.providerKey, selected.modelId)
|
|
1315
|
-
const newCursor = state.visibleSorted.findIndex(r => toFavoriteKey(r.providerKey, r.modelId) === selectedKey)
|
|
1316
|
-
if (newCursor >= 0) state.cursor = newCursor
|
|
1317
|
-
else if (state.cursor >= state.visibleSorted.length) state.cursor = Math.max(0, state.visibleSorted.length - 1)
|
|
1318
|
-
adjustScrollOffset(state)
|
|
1699
|
+
toggleFavoriteOnSelectedRow()
|
|
1319
1700
|
return
|
|
1320
1701
|
}
|
|
1321
1702
|
|
|
1322
1703
|
// 📖 I key: open Feedback overlay (anonymous Discord feedback)
|
|
1323
1704
|
if (key.name === 'i') {
|
|
1324
|
-
|
|
1325
|
-
state.bugReportBuffer = ''
|
|
1326
|
-
state.bugReportStatus = 'idle'
|
|
1327
|
-
state.bugReportError = null
|
|
1705
|
+
openFeedbackOverlay()
|
|
1328
1706
|
return
|
|
1329
1707
|
}
|
|
1330
1708
|
|
|
@@ -1399,13 +1777,7 @@ export function createKeyHandler(ctx) {
|
|
|
1399
1777
|
|
|
1400
1778
|
// 📖 Mode toggle key: Z cycles through the supported tool targets.
|
|
1401
1779
|
if (key.name === 'z') {
|
|
1402
|
-
|
|
1403
|
-
const currentIndex = modeOrder.indexOf(state.mode)
|
|
1404
|
-
const nextIndex = (currentIndex + 1) % modeOrder.length
|
|
1405
|
-
state.mode = modeOrder[nextIndex]
|
|
1406
|
-
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
1407
|
-
state.config.settings.preferredToolMode = state.mode
|
|
1408
|
-
saveConfig(state.config)
|
|
1780
|
+
cycleToolMode()
|
|
1409
1781
|
return
|
|
1410
1782
|
}
|
|
1411
1783
|
|
|
@@ -1442,46 +1814,24 @@ export function createKeyHandler(ctx) {
|
|
|
1442
1814
|
// 📖 Use the cached visible+sorted array — guaranteed to match what's on screen
|
|
1443
1815
|
const selected = state.visibleSorted[state.cursor]
|
|
1444
1816
|
if (!selected) return // 📖 Guard: empty visible list (all filtered out)
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
} else if (selected.status === 'down') {
|
|
1457
|
-
console.log(chalk.red(` ⚠ Selected: ${selected.label} (currently down)`))
|
|
1458
|
-
} else {
|
|
1459
|
-
console.log(chalk.cyan(` ✓ Selected: ${selected.label}`))
|
|
1460
|
-
}
|
|
1461
|
-
console.log()
|
|
1462
|
-
|
|
1463
|
-
// 📖 Warn if no API key is configured for the selected model's provider
|
|
1464
|
-
if (state.mode !== 'openclaw') {
|
|
1465
|
-
const selectedApiKey = getApiKey(state.config, selected.providerKey)
|
|
1466
|
-
if (!selectedApiKey) {
|
|
1467
|
-
console.log(chalk.yellow(` Warning: No API key configured for ${selected.providerKey}.`))
|
|
1468
|
-
console.log(chalk.yellow(` The selected tool may not be able to use ${selected.label}.`))
|
|
1469
|
-
console.log(chalk.dim(` Set ${ENV_VAR_NAMES[selected.providerKey] || selected.providerKey.toUpperCase() + '_API_KEY'} or configure via settings (P key).`))
|
|
1470
|
-
console.log()
|
|
1817
|
+
if (shouldCheckMissingTool(state.mode) && !isToolInstalled(state.mode)) {
|
|
1818
|
+
state.toolInstallPromptOpen = true
|
|
1819
|
+
state.toolInstallPromptCursor = 0
|
|
1820
|
+
state.toolInstallPromptScrollOffset = 0
|
|
1821
|
+
state.toolInstallPromptMode = state.mode
|
|
1822
|
+
state.toolInstallPromptModel = {
|
|
1823
|
+
modelId: selected.modelId,
|
|
1824
|
+
label: selected.label,
|
|
1825
|
+
tier: selected.tier,
|
|
1826
|
+
providerKey: selected.providerKey,
|
|
1827
|
+
status: selected.status,
|
|
1471
1828
|
}
|
|
1829
|
+
state.toolInstallPromptPlan = getToolInstallPlan(state.mode)
|
|
1830
|
+
state.toolInstallPromptErrorMsg = null
|
|
1831
|
+
return
|
|
1472
1832
|
}
|
|
1473
1833
|
|
|
1474
|
-
|
|
1475
|
-
if (state.mode === 'openclaw') {
|
|
1476
|
-
await startOpenClaw(userSelected, state.config)
|
|
1477
|
-
} else if (state.mode === 'opencode-desktop') {
|
|
1478
|
-
await startOpenCodeDesktop(userSelected, state.config)
|
|
1479
|
-
} else if (state.mode === 'opencode') {
|
|
1480
|
-
await startOpenCode(userSelected, state.config)
|
|
1481
|
-
} else {
|
|
1482
|
-
await startExternalTool(state.mode, userSelected, state.config)
|
|
1483
|
-
}
|
|
1484
|
-
process.exit(0)
|
|
1834
|
+
await launchSelectedModel(selected)
|
|
1485
1835
|
}
|
|
1486
1836
|
}
|
|
1487
1837
|
}
|