free-coding-models 0.3.51 → 0.3.54
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 +18 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/app.js +27 -8
- package/src/command-palette.js +2 -0
- package/src/constants.js +10 -2
- package/src/endpoint-installer.js +1 -1
- package/src/installed-models-manager.js +53 -1
- package/src/key-handler.js +26 -1
- package/src/kilo-config.js +43 -0
- package/src/kilo.js +200 -0
- package/src/opencode.js +56 -0
- package/src/render-table.js +209 -127
- package/src/shell-env.js +3 -0
- package/src/tool-bootstrap.js +10 -0
- package/src/tool-metadata.js +7 -2
- package/src/updater.js +2 -0
- package/src/utils.js +7 -3
- package/web/dist/assets/{index-CJjHD8Iz.js → index-D2ban2S-.js} +1 -1
- package/web/dist/index.html +1 -1
package/src/opencode.js
CHANGED
|
@@ -543,6 +543,62 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
543
543
|
await spawnOpenCode(['--model', modelRef], providerKey, fcmConfig)
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
+
// ─── Start OpenCode Web ───────────────────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
export async function startOpenCodeWeb(model, fcmConfig) {
|
|
549
|
+
const providerKey = model.providerKey ?? 'nvidia'
|
|
550
|
+
const ocModelId = getOpenCodeModelId(providerKey, model.modelId)
|
|
551
|
+
const modelRef = `${providerKey}/${ocModelId}`
|
|
552
|
+
|
|
553
|
+
console.log(chalk.green(` Setting ${chalk.bold(model.label)} as default...`))
|
|
554
|
+
console.log(chalk.dim(` Model: ${modelRef}`))
|
|
555
|
+
console.log()
|
|
556
|
+
|
|
557
|
+
const config = loadOpenCodeConfig()
|
|
558
|
+
const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
|
|
559
|
+
|
|
560
|
+
if (existsSync(getOpenCodeConfigPath())) {
|
|
561
|
+
copyFileSync(getOpenCodeConfigPath(), backupPath)
|
|
562
|
+
console.log(chalk.dim(` Backup: ${backupPath}`))
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!config.provider) config.provider = {}
|
|
566
|
+
|
|
567
|
+
// 📖 Provider-specific config setup (same as CLI/Desktop)
|
|
568
|
+
if (providerKey === 'nvidia' && !config.provider.nvidia) {
|
|
569
|
+
config.provider.nvidia = {
|
|
570
|
+
npm: '@ai-sdk/openai-compatible',
|
|
571
|
+
name: 'NVIDIA NIM',
|
|
572
|
+
options: { baseURL: 'https://integrate.api.nvidia.com/v1', apiKey: '{env:NVIDIA_API_KEY}' },
|
|
573
|
+
models: {}
|
|
574
|
+
}
|
|
575
|
+
} else if (providerKey === 'groq' && !config.provider.groq) {
|
|
576
|
+
config.provider.groq = { options: { apiKey: '{env:GROQ_API_KEY}' }, models: {} }
|
|
577
|
+
} else if (providerKey === 'cerebras' && !config.provider.cerebras) {
|
|
578
|
+
config.provider.cerebras = {
|
|
579
|
+
npm: '@ai-sdk/openai-compatible',
|
|
580
|
+
name: 'Cerebras',
|
|
581
|
+
options: { baseURL: 'https://api.cerebras.ai/v1', apiKey: '{env:CEREBRAS_API_KEY}' },
|
|
582
|
+
models: {}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// ... other providers are handled as they are selected
|
|
586
|
+
|
|
587
|
+
if (providerKey !== 'opencode-zen' && config.provider[providerKey]) {
|
|
588
|
+
if (!config.provider[providerKey].models) config.provider[providerKey].models = {}
|
|
589
|
+
config.provider[providerKey].models[ocModelId] = { name: model.label }
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
config.model = providerKey === 'opencode-zen' ? `opencode/${ocModelId}` : modelRef
|
|
593
|
+
saveOpenCodeConfig(config)
|
|
594
|
+
|
|
595
|
+
console.log(chalk.dim(` Config saved to: ${getOpenCodeConfigPath()}`))
|
|
596
|
+
console.log(chalk.dim(' Starting OpenCode Web...'))
|
|
597
|
+
console.log()
|
|
598
|
+
|
|
599
|
+
await spawnOpenCode(['web', '--model', modelRef], providerKey, fcmConfig)
|
|
600
|
+
}
|
|
601
|
+
|
|
546
602
|
// ─── Start OpenCode Desktop ───────────────────────────────────────────────────
|
|
547
603
|
|
|
548
604
|
export async function startOpenCodeDesktop(model, fcmConfig) {
|
package/src/render-table.js
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
import { themeColors, getProviderRgb, getTierRgb, getReadableTextRgb, getTheme } from './theme.js'
|
|
49
49
|
import { TIER_COLOR } from './tier-colors.js'
|
|
50
50
|
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
|
|
51
|
+
import { VERDICT_CYCLE } from './constants.js'
|
|
51
52
|
import { usagePlaceholderForProvider } from './ping.js'
|
|
52
53
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
|
|
53
54
|
import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER, isModelCompatibleWithTool } from './tool-metadata.js'
|
|
@@ -57,6 +58,10 @@ import { detectPackageManager, getManualInstallCmd } from './updater.js'
|
|
|
57
58
|
const require = createRequire(import.meta.url)
|
|
58
59
|
const { version: LOCAL_VERSION } = require('../package.json')
|
|
59
60
|
|
|
61
|
+
// 📖 HEALTH_CYCLE: cycles through health/status states (local constant for render-table.js)
|
|
62
|
+
// VERDICT_CYCLE is now imported from constants.js
|
|
63
|
+
const HEALTH_CYCLE = [null, 'up', 'timeout', 'down', 'auth_error', 'noauth', 'pending']
|
|
64
|
+
|
|
60
65
|
// 📖 Mouse support: column boundary map updated every frame by renderTable().
|
|
61
66
|
// 📖 Each entry maps a column name to its display X-start and X-end (1-based, inclusive).
|
|
62
67
|
// 📖 headerRow is the 1-based terminal row of the column header line.
|
|
@@ -104,7 +109,7 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
104
109
|
})
|
|
105
110
|
|
|
106
111
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
107
|
-
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null, footerHidden = false) {
|
|
112
|
+
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null, footerHidden = false, verdictFilterMode = 0, healthFilterMode = 0) {
|
|
108
113
|
// 📖 Filter out hidden models for display
|
|
109
114
|
const visibleResults = results.filter(r => !r.hidden)
|
|
110
115
|
|
|
@@ -321,6 +326,77 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
321
326
|
'',
|
|
322
327
|
]
|
|
323
328
|
|
|
329
|
+
// 📖 Filter bar — llmfit-style horizontal filter pills (1 dedicated row above table)
|
|
330
|
+
// 📖 Each block: title with hotkey hint + active value colored by filter state
|
|
331
|
+
{
|
|
332
|
+
const filterParts = []
|
|
333
|
+
const filterSep = themeColors.dim(' │ ')
|
|
334
|
+
const blockSep = ' │ '
|
|
335
|
+
|
|
336
|
+
// 📖 Search filter block — shows active text filter or prompt
|
|
337
|
+
if (customTextFilter && customTextFilter.trim()) {
|
|
338
|
+
const badgeText = ` Search "/" ${blockSep} ${customTextFilter.trim().slice(0, 20)} `
|
|
339
|
+
filterParts.push(themeColors.badge(badgeText, [52, 120, 88], [255, 255, 255]))
|
|
340
|
+
} else {
|
|
341
|
+
filterParts.push(themeColors.dim(' Search "/" '))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 📖 Tier filter block — T key cycles through TIER_CYCLE
|
|
345
|
+
if (tierFilterMode > 0) {
|
|
346
|
+
const tierLabel = TIER_CYCLE_NAMES[tierFilterMode]
|
|
347
|
+
const tierBg = getTierRgb(tierLabel)
|
|
348
|
+
filterParts.push(themeColors.badge(` Tier (${tierLabel}) `, tierBg, [255, 255, 255]))
|
|
349
|
+
} else {
|
|
350
|
+
filterParts.push(themeColors.dim(' Tier (T) '))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 📖 Provider filter block — D key cycles through providers
|
|
354
|
+
if (originFilterMode > 0) {
|
|
355
|
+
const originKeys = [null, ...Object.keys(sources)]
|
|
356
|
+
const activeOriginKey = originKeys[originFilterMode]
|
|
357
|
+
const activeOriginName = activeOriginKey ? sources[activeOriginKey]?.name ?? activeOriginKey : null
|
|
358
|
+
if (activeOriginName) {
|
|
359
|
+
const normName = normalizeOriginLabel(activeOriginName, activeOriginKey)
|
|
360
|
+
const providerRgb = PROVIDER_COLOR[activeOriginKey] || [255, 255, 255]
|
|
361
|
+
filterParts.push(themeColors.badge(` Provider (${normName}) `, providerRgb, [255, 255, 255]))
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
filterParts.push(themeColors.dim(' Provider (D) '))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 📖 Verdict filter block — V key cycles through verdicts
|
|
368
|
+
if (verdictFilterMode > 0) {
|
|
369
|
+
const verdictLabel = VERDICT_CYCLE[verdictFilterMode]
|
|
370
|
+
const verdictColors = {
|
|
371
|
+
'Perfect': themeColors.success,
|
|
372
|
+
'Normal': themeColors.metricGood,
|
|
373
|
+
'Slow': (t) => chalk.bold.rgb(...getTierRgb('A-'))(t),
|
|
374
|
+
'Spiky': (t) => chalk.bold.rgb(...getTierRgb('A+'))(t),
|
|
375
|
+
'Very Slow': (t) => chalk.bold.rgb(...getTierRgb('B+'))(t),
|
|
376
|
+
'Overloaded': (t) => chalk.bold.rgb(...getTierRgb('B'))(t),
|
|
377
|
+
'Unstable': themeColors.errorBold,
|
|
378
|
+
'Not Active': themeColors.dim,
|
|
379
|
+
'Pending': themeColors.dim,
|
|
380
|
+
}
|
|
381
|
+
const vc = verdictColors[verdictLabel] || themeColors.accent
|
|
382
|
+
filterParts.push(themeColors.badge(` Verdict (${verdictLabel}) `, [20, 20, 20], vc === themeColors.dim ? [130, 130, 130] : [255, 255, 255]))
|
|
383
|
+
} else {
|
|
384
|
+
filterParts.push(themeColors.dim(' Verdict (V) '))
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 📖 Health filter block — H key cycles through health states
|
|
388
|
+
if (healthFilterMode > 0) {
|
|
389
|
+
const healthLabel = HEALTH_CYCLE[healthFilterMode]
|
|
390
|
+
const healthDisplay = healthLabel === 'auth_error' ? 'Auth Err' : healthLabel === 'noauth' ? 'No Key' : healthLabel.charAt(0).toUpperCase() + healthLabel.slice(1)
|
|
391
|
+
const healthBg = healthLabel === 'up' ? [52, 120, 88] : healthLabel === 'timeout' ? [180, 130, 0] : healthLabel === 'down' ? [120, 40, 40] : [60, 60, 60]
|
|
392
|
+
filterParts.push(themeColors.badge(` Health (${healthDisplay}) `, healthBg, [255, 255, 255]))
|
|
393
|
+
} else {
|
|
394
|
+
filterParts.push(themeColors.dim(' Health (H) '))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
lines.push(filterParts.join(blockSep))
|
|
398
|
+
}
|
|
399
|
+
|
|
324
400
|
// 📖 Header row with sorting indicators
|
|
325
401
|
// 📖 NOTE: padEnd on chalk strings counts ANSI codes, breaking alignment
|
|
326
402
|
// 📖 Solution: build plain text first, then colorize
|
|
@@ -756,135 +832,128 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
756
832
|
}
|
|
757
833
|
}
|
|
758
834
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
)
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
{
|
|
783
|
-
const cpText = ' CTRL+P ⚡️ Command Palette '
|
|
784
|
-
const parts = [
|
|
785
|
-
{ text: ' ', key: null },
|
|
786
|
-
{ text: cpText, key: 'ctrl+p' },
|
|
787
|
-
{ text: ' • ', key: null },
|
|
788
|
-
{ text: 'Q Smart Recommend', key: 'q' },
|
|
789
|
-
{ text: ' • ', key: null },
|
|
790
|
-
{ text: 'G Theme', key: 'g' },
|
|
791
|
-
{ text: ' • ', key: null },
|
|
792
|
-
{ text: 'I Feedback, bugs & requests', key: 'i' },
|
|
793
|
-
]
|
|
794
|
-
const footerRow2 = lines.length + 1
|
|
795
|
-
let xPos = 1
|
|
796
|
-
for (const part of parts) {
|
|
797
|
-
const w = displayWidth(part.text)
|
|
798
|
-
if (part.key) footerHotkeys.push({ key: part.key, row: footerRow2, xStart: xPos, xEnd: xPos + w - 1 })
|
|
799
|
-
xPos += w
|
|
800
|
-
}
|
|
801
|
-
}
|
|
835
|
+
if (!footerHidden) {
|
|
836
|
+
// 📖 Full footer — all hint lines hidden when footerHidden=true to maximize table space
|
|
837
|
+
lines.push(
|
|
838
|
+
' ' + hotkey('F', ' Toggle Favorite') +
|
|
839
|
+
themeColors.dim(` • `) +
|
|
840
|
+
activeHotkey('Y', favoritesModeLabel, favoritesModeBg) +
|
|
841
|
+
themeColors.dim(` • `) +
|
|
842
|
+
(tierFilterMode > 0
|
|
843
|
+
? activeHotkey('T', ` Tier (${activeTierLabel})`, getTierRgb(activeTierLabel))
|
|
844
|
+
: hotkey('T', ' Tier')) +
|
|
845
|
+
themeColors.dim(` • `) +
|
|
846
|
+
(originFilterMode > 0
|
|
847
|
+
? activeHotkey('D', ` Provider (${activeOriginLabel})`, PROVIDER_COLOR[[null, ...Object.keys(sources)][originFilterMode]] || [255, 255, 255])
|
|
848
|
+
: hotkey('D', ' Provider')) +
|
|
849
|
+
themeColors.dim(` • `) +
|
|
850
|
+
(hideUnconfiguredModels ? activeHotkey('E', ' Show only configured models', configuredBadgeBg) : hotkey('E', ' Show only configured models')) +
|
|
851
|
+
themeColors.dim(` • `) +
|
|
852
|
+
hotkey('P', ' Settings') +
|
|
853
|
+
themeColors.dim(` • `) +
|
|
854
|
+
themeColors.dim('J/K Navigate') +
|
|
855
|
+
themeColors.dim(` • `) +
|
|
856
|
+
themeColors.dim('Ctrl+H Help')
|
|
857
|
+
)
|
|
802
858
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
lines.push(footerLine)
|
|
825
|
-
|
|
826
|
-
if (versionStatus.isOutdated) {
|
|
827
|
-
const updateMsg = ` 🚀⬆️ UPDATE AVAILABLE — v${LOCAL_VERSION} → v${versionStatus.latestVersion} • Click here or press Shift+U to update 🚀⬆️ `
|
|
828
|
-
const paddedBanner = terminalCols > 0
|
|
829
|
-
? updateMsg + ' '.repeat(Math.max(0, terminalCols - displayWidth(updateMsg)))
|
|
830
|
-
: updateMsg
|
|
831
|
-
const fluoGreenBanner = chalk.bgRgb(57, 255, 20).rgb(0, 0, 0).bold(paddedBanner)
|
|
832
|
-
const updateBannerRow = lines.length + 1
|
|
833
|
-
_lastLayout.updateBannerRow = updateBannerRow
|
|
834
|
-
footerHotkeys.push({ key: 'update-click', row: updateBannerRow, xStart: 1, xEnd: Math.max(terminalCols, displayWidth(updateMsg)) })
|
|
835
|
-
lines.push(fluoGreenBanner)
|
|
836
|
-
} else {
|
|
837
|
-
_lastLayout.updateBannerRow = 0
|
|
838
|
-
}
|
|
859
|
+
// 📖 Line 2: command palette, recommend, feedback, theme
|
|
860
|
+
{
|
|
861
|
+
const cpText = ' CTRL+P ⚡️ Command Palette '
|
|
862
|
+
const parts = [
|
|
863
|
+
{ text: ' ', key: null },
|
|
864
|
+
{ text: cpText, key: 'ctrl+p' },
|
|
865
|
+
{ text: ' • ', key: null },
|
|
866
|
+
{ text: 'Q Smart Recommend', key: 'q' },
|
|
867
|
+
{ text: ' • ', key: null },
|
|
868
|
+
{ text: 'G Theme', key: 'g' },
|
|
869
|
+
{ text: ' • ', key: null },
|
|
870
|
+
{ text: 'I Feedback, bugs & requests', key: 'i' },
|
|
871
|
+
]
|
|
872
|
+
const footerRow2 = lines.length + 1
|
|
873
|
+
let xPos = 1
|
|
874
|
+
for (const part of parts) {
|
|
875
|
+
const w = displayWidth(part.text)
|
|
876
|
+
if (part.key) footerHotkeys.push({ key: part.key, row: footerRow2, xStart: xPos, xEnd: xPos + w - 1 })
|
|
877
|
+
xPos += w
|
|
878
|
+
}
|
|
879
|
+
}
|
|
839
880
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
881
|
+
// 📖 Line 2: command palette (highlighted as new), recommend, feedback, and extended hints.
|
|
882
|
+
// 📖 CTRL+P ⚡️ Command Palette uses neon-green-on-dark-green background to highlight the feature.
|
|
883
|
+
const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold(' CTRL+P ⚡️ Command Palette ')
|
|
884
|
+
lines.push(
|
|
885
|
+
' ' + paletteLabel + themeColors.dim(` • `) +
|
|
886
|
+
hotkey('Q', ' Smart Recommend') + themeColors.dim(` • `) +
|
|
887
|
+
hotkey('G', ' Theme') + themeColors.dim(` • `) +
|
|
888
|
+
hotkey('I', ' Feedback, bugs & requests')
|
|
889
|
+
)
|
|
890
|
+
// 📖 Proxy status is now shown via the badge in line 2 above — no need for a dedicated line
|
|
891
|
+
const footerLine =
|
|
892
|
+
themeColors.footerLove(' Made with 💖 & ☕ by \x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
|
|
893
|
+
themeColors.dim(' • ') +
|
|
894
|
+
'⭐ ' +
|
|
895
|
+
themeColors.link('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\Star on GitHub\x1b]8;;\x1b\\') +
|
|
896
|
+
themeColors.dim(' • ') +
|
|
897
|
+
'🤝 ' +
|
|
898
|
+
themeColors.warning('\x1b]8;;https://github.com/vava-nessa/free-coding-models/graphs/contributors\x1b\\Contributors\x1b]8;;\x1b\\') +
|
|
899
|
+
themeColors.dim(' • ') +
|
|
900
|
+
'☕ ' +
|
|
901
|
+
themeColors.footerCoffee('\x1b]8;;https://buymeacoffee.com/vavanessadev\x1b\\Buy me a coffee\x1b]8;;\x1b\\')
|
|
902
|
+
lines.push(footerLine)
|
|
903
|
+
|
|
904
|
+
if (versionStatus.isOutdated) {
|
|
905
|
+
const updateMsg = ` 🚀⬆️ UPDATE AVAILABLE — v${LOCAL_VERSION} → v${versionStatus.latestVersion} • Click here or press Shift+U to update 🚀⬆️ `
|
|
906
|
+
const paddedBanner = terminalCols > 0
|
|
907
|
+
? updateMsg + ' '.repeat(Math.max(0, terminalCols - displayWidth(updateMsg)))
|
|
908
|
+
: updateMsg
|
|
909
|
+
const fluoGreenBanner = chalk.bgRgb(57, 255, 20).rgb(0, 0, 0).bold(paddedBanner)
|
|
910
|
+
const updateBannerRow = lines.length + 1
|
|
911
|
+
_lastLayout.updateBannerRow = updateBannerRow
|
|
912
|
+
footerHotkeys.push({ key: 'update-click', row: updateBannerRow, xStart: 1, xEnd: Math.max(terminalCols, displayWidth(updateMsg)) })
|
|
913
|
+
lines.push(fluoGreenBanner)
|
|
914
|
+
} else {
|
|
915
|
+
_lastLayout.updateBannerRow = 0
|
|
916
|
+
}
|
|
857
917
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const lastFooterRow = lines.length + 1 // 📖 1-based terminal row (line about to be pushed)
|
|
861
|
-
const parts = [
|
|
862
|
-
{ text: ' ', key: null },
|
|
863
|
-
{ text: 'N Changelog', key: 'n' },
|
|
864
|
-
]
|
|
918
|
+
// 📖 Final footer line: changelog + optional active text-filter badge + exit hint.
|
|
919
|
+
let filterBadge = ''
|
|
865
920
|
if (hasCustomFilter) {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
921
|
+
const normalizedFilter = customTextFilter.trim().replace(/\s+/g, ' ')
|
|
922
|
+
const filterPrefix = 'X Disable filter: "'
|
|
923
|
+
const filterSuffix = '"'
|
|
924
|
+
const separatorPlain = ' • '
|
|
925
|
+
const baseFooterPlain = ' N Changelog' + separatorPlain + 'Ctrl+C Exit'
|
|
926
|
+
const baseBadgeWidth = displayWidth(` ${filterPrefix}${filterSuffix} `)
|
|
927
|
+
const availableFilterWidth = terminalCols > 0
|
|
928
|
+
? Math.max(8, terminalCols - displayWidth(baseFooterPlain) - displayWidth(separatorPlain) - baseBadgeWidth)
|
|
929
|
+
: normalizedFilter.length
|
|
930
|
+
const visibleFilter = normalizedFilter.length > availableFilterWidth
|
|
931
|
+
? `${normalizedFilter.slice(0, Math.max(3, availableFilterWidth - 3))}...`
|
|
932
|
+
: normalizedFilter
|
|
933
|
+
filterBadge = chalk.bgYellow.black.bold(` ${filterPrefix}${visibleFilter}${filterSuffix} `)
|
|
876
934
|
}
|
|
877
|
-
}
|
|
878
935
|
|
|
879
|
-
|
|
936
|
+
// 📖 Mouse support: track last footer line hotkey zones
|
|
937
|
+
{
|
|
938
|
+
const lastFooterRow = lines.length + 1 // 📖 1-based terminal row (line about to be pushed)
|
|
939
|
+
const parts = [
|
|
940
|
+
{ text: ' ', key: null },
|
|
941
|
+
{ text: 'N Changelog', key: 'n' },
|
|
942
|
+
]
|
|
943
|
+
if (hasCustomFilter) {
|
|
944
|
+
parts.push({ text: ' • ', key: null })
|
|
945
|
+
// 📖 X key clears filter — compute width from rendered badge text
|
|
946
|
+
const badgePlain = `X Disable filter: "${customTextFilter.trim().replace(/\s+/g, ' ')}"`
|
|
947
|
+
parts.push({ text: ` ${badgePlain} `, key: 'x' })
|
|
948
|
+
}
|
|
949
|
+
let xPos = 1
|
|
950
|
+
for (const part of parts) {
|
|
951
|
+
const w = displayWidth(part.text)
|
|
952
|
+
if (part.key) footerHotkeys.push({ key: part.key, row: lastFooterRow, xStart: xPos, xEnd: xPos + w - 1 })
|
|
953
|
+
xPos += w
|
|
954
|
+
}
|
|
955
|
+
}
|
|
880
956
|
|
|
881
|
-
if (footerHidden) {
|
|
882
|
-
// 📖 Collapsed footer: single line with toggle hint
|
|
883
|
-
lines.push(
|
|
884
|
-
' ' + themeColors.hotkey('Ctrl+O') + themeColors.dim(' Toggle Footer') +
|
|
885
|
-
themeColors.dim(' • Ctrl+C Exit')
|
|
886
|
-
)
|
|
887
|
-
} else {
|
|
888
957
|
const releaseLabel = lastReleaseDate
|
|
889
958
|
? chalk.rgb(255, 182, 193)(`Last release: ${lastReleaseDate}`)
|
|
890
959
|
: ''
|
|
@@ -906,14 +975,27 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
906
975
|
themeColors.dim(' → ') +
|
|
907
976
|
themeColors.footerDiscord('https://discord.gg/ZTNFHvvCkU')
|
|
908
977
|
)
|
|
978
|
+
} else {
|
|
979
|
+
// 📖 Collapsed footer: single line with toggle hint
|
|
980
|
+
lines.push(
|
|
981
|
+
' ' + themeColors.hotkey('Ctrl+O') + themeColors.dim(' Toggle Footer') +
|
|
982
|
+
themeColors.dim(' • Ctrl+C Exit')
|
|
983
|
+
)
|
|
909
984
|
}
|
|
910
985
|
|
|
986
|
+
_lastLayout.footerHotkeys = footerHotkeys
|
|
987
|
+
|
|
911
988
|
// 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
|
|
912
|
-
// 📖 frames are cleared.
|
|
913
|
-
// 📖
|
|
989
|
+
// 📖 frames are cleared. \x1b[J (erase from cursor to end of screen) clears any
|
|
990
|
+
// 📖 stale content below when footer is hidden.
|
|
914
991
|
const EL = '\x1b[K'
|
|
915
992
|
const cleared = lines.map(l => l + EL)
|
|
916
|
-
|
|
917
|
-
|
|
993
|
+
if (footerHidden) {
|
|
994
|
+
// 📖 When footer is hidden, \x1b[J erases stale footer content below the cursor
|
|
995
|
+
cleared.push('\x1b[J')
|
|
996
|
+
} else {
|
|
997
|
+
const remaining = terminalRows > 0 ? Math.max(0, terminalRows - cleared.length) : 0
|
|
998
|
+
for (let i = 0; i < remaining; i++) cleared.push(EL)
|
|
999
|
+
}
|
|
918
1000
|
return cleared.join('\n')
|
|
919
1001
|
}
|
package/src/shell-env.js
CHANGED
|
@@ -41,6 +41,7 @@ import { join } from 'node:path'
|
|
|
41
41
|
import * as readline from 'node:readline'
|
|
42
42
|
import chalk from 'chalk'
|
|
43
43
|
import { ENV_VAR_NAMES } from './provider-metadata.js'
|
|
44
|
+
import { saveConfig } from './config.js'
|
|
44
45
|
|
|
45
46
|
// 📖 Unique marker used to identify the source line we inject into shell rc files.
|
|
46
47
|
// 📖 This allows idempotent add/remove without relying on exact path matching.
|
|
@@ -361,6 +362,8 @@ export async function promptShellEnvMigration(config) {
|
|
|
361
362
|
render()
|
|
362
363
|
|
|
363
364
|
readline.emitKeypressEvents(process.stdin)
|
|
365
|
+
// 📖 Ensure stdin is flowing — a prior prompt may have paused it
|
|
366
|
+
process.stdin.resume()
|
|
364
367
|
if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
365
368
|
|
|
366
369
|
const onKey = (_str, key) => {
|
package/src/tool-bootstrap.js
CHANGED
|
@@ -173,6 +173,16 @@ export const TOOL_BOOTSTRAP_METADATA = {
|
|
|
173
173
|
},
|
|
174
174
|
},
|
|
175
175
|
},
|
|
176
|
+
kilo: {
|
|
177
|
+
binary: 'kilo',
|
|
178
|
+
docsUrl: 'https://kilo.ai/docs/cli',
|
|
179
|
+
install: {
|
|
180
|
+
default: {
|
|
181
|
+
shellCommand: 'npm install -g @kilocode/cli',
|
|
182
|
+
summary: 'Install Kilo CLI globally via npm.',
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
176
186
|
qwen: {
|
|
177
187
|
binary: 'qwen',
|
|
178
188
|
docsUrl: 'https://qwenlm.github.io/qwen-code-docs/en/users/quickstart/',
|
package/src/tool-metadata.js
CHANGED
|
@@ -28,11 +28,13 @@
|
|
|
28
28
|
export const TOOL_METADATA = {
|
|
29
29
|
opencode: { label: 'OpenCode CLI', emoji: '📦', flag: '--opencode', color: [110, 214, 255] },
|
|
30
30
|
'opencode-desktop': { label: 'OpenCode Desktop', emoji: '📦', flag: '--opencode-desktop', color: [149, 205, 255] },
|
|
31
|
+
'opencode-web': { label: 'OpenCode Web', emoji: '📦', flag: '--opencode-web', color: [180, 220, 255] },
|
|
31
32
|
openclaw: { label: 'OpenClaw', emoji: '🦞', flag: '--openclaw', color: [255, 129, 129] },
|
|
32
33
|
crush: { label: 'Crush', emoji: '💘', flag: '--crush', color: [255, 168, 209] },
|
|
33
34
|
goose: { label: 'Goose', emoji: '🪿', flag: '--goose', color: [132, 235, 168] },
|
|
34
35
|
pi: { label: 'Pi', emoji: 'π', flag: '--pi', color: [173, 216, 230] },
|
|
35
36
|
aider: { label: 'Aider', emoji: '🛠', flag: '--aider', color: [255, 208, 102] },
|
|
37
|
+
kilo: { label: 'Kilo CLI', emoji: '⚡️', flag: '--kilo', color: [255, 107, 107] },
|
|
36
38
|
qwen: { label: 'Qwen Code', emoji: '🐉', flag: '--qwen', color: [255, 213, 128] },
|
|
37
39
|
openhands: { label: 'OpenHands', emoji: '🤲', flag: '--openhands', color: [228, 191, 239] },
|
|
38
40
|
amp: { label: 'Amp', emoji: '⚡', flag: '--amp', color: [255, 232, 98] },
|
|
@@ -49,12 +51,13 @@ export const TOOL_METADATA = {
|
|
|
49
51
|
// 📖 OpenCode CLI + Desktop are merged into a single 📦 slot since they share compatibility.
|
|
50
52
|
// 📖 Each slot maps to one or more toolKeys for compatibility checking.
|
|
51
53
|
export const COMPAT_COLUMN_SLOTS = [
|
|
52
|
-
{ emoji: '📦', toolKeys: ['opencode', 'opencode-desktop'], color: [110, 214, 255] },
|
|
54
|
+
{ emoji: '📦', toolKeys: ['opencode', 'opencode-desktop', 'opencode-web'], color: [110, 214, 255] },
|
|
53
55
|
{ emoji: '🦞', toolKeys: ['openclaw'], color: [255, 129, 129] },
|
|
54
56
|
{ emoji: '💘', toolKeys: ['crush'], color: [255, 168, 209] },
|
|
55
57
|
{ emoji: '🪿', toolKeys: ['goose'], color: [132, 235, 168] },
|
|
56
58
|
{ emoji: 'π', toolKeys: ['pi'], color: [173, 216, 230] },
|
|
57
59
|
{ emoji: '🛠', toolKeys: ['aider'], color: [255, 208, 102] },
|
|
60
|
+
{ emoji: '⚡️', toolKeys: ['kilo'], color: [255, 107, 107] },
|
|
58
61
|
{ emoji: '🐉', toolKeys: ['qwen'], color: [255, 213, 128] },
|
|
59
62
|
{ emoji: '🤲', toolKeys: ['openhands'], color: [228, 191, 239] },
|
|
60
63
|
{ emoji: '⚡', toolKeys: ['amp'], color: [255, 232, 98] },
|
|
@@ -72,10 +75,12 @@ export const TOOL_MODE_ORDER = [
|
|
|
72
75
|
'pi',
|
|
73
76
|
'jcode',
|
|
74
77
|
'opencode-desktop',
|
|
78
|
+
'opencode-web',
|
|
75
79
|
'openclaw',
|
|
76
80
|
'crush',
|
|
77
81
|
'goose',
|
|
78
82
|
'aider',
|
|
83
|
+
'kilo',
|
|
79
84
|
'qwen',
|
|
80
85
|
'openhands',
|
|
81
86
|
'amp',
|
|
@@ -100,7 +105,7 @@ export function getToolModeOrder() {
|
|
|
100
105
|
const REGULAR_TOOLS = Object.keys(TOOL_METADATA).filter(k => !TOOL_METADATA[k].cliOnly)
|
|
101
106
|
|
|
102
107
|
// 📖 Zen-only tools: OpenCode Zen models can ONLY run on OpenCode CLI / OpenCode Desktop.
|
|
103
|
-
const ZEN_COMPATIBLE_TOOLS = ['opencode', 'opencode-desktop']
|
|
108
|
+
const ZEN_COMPATIBLE_TOOLS = ['opencode', 'opencode-desktop', 'opencode-web']
|
|
104
109
|
|
|
105
110
|
/**
|
|
106
111
|
* 📖 Returns the list of tool keys a model is compatible with.
|
package/src/updater.js
CHANGED
|
@@ -394,6 +394,8 @@ export async function promptUpdateNotification(latestVersion) {
|
|
|
394
394
|
render()
|
|
395
395
|
|
|
396
396
|
readline.emitKeypressEvents(process.stdin)
|
|
397
|
+
// 📖 Ensure stdin is flowing — the shell-env prompt may have paused it
|
|
398
|
+
process.stdin.resume()
|
|
397
399
|
if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
398
400
|
|
|
399
401
|
const onKey = (_str, key) => {
|
package/src/utils.js
CHANGED
|
@@ -387,13 +387,13 @@ export function findBestModel(results) {
|
|
|
387
387
|
//
|
|
388
388
|
// 📖 Argument types:
|
|
389
389
|
// - API key: first positional arg that does not look like a CLI flag (e.g., "nvapi-xxx")
|
|
390
|
-
// - Boolean flags: --best, --fiable, --opencode, --opencode-desktop, --openclaw,
|
|
391
|
-
// --aider, --crush, --goose, --qwen,
|
|
390
|
+
// - Boolean flags: --best, --fiable, --opencode, --opencode-desktop, --opencode-web, --openclaw,
|
|
391
|
+
// --aider, --crush, --goose, --qwen, --kilo,
|
|
392
392
|
// --openhands, --amp, --pi, --no-telemetry, --json, --help/-h (case-insensitive)
|
|
393
393
|
// - Value flag: --tier <letter> (the next non-flag arg is the tier value)
|
|
394
394
|
//
|
|
395
395
|
// Returns:
|
|
396
|
-
// { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode,
|
|
396
|
+
// { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openCodeWebMode, openClawMode,
|
|
397
397
|
// aiderMode, crushMode, gooseMode, qwenMode, openHandsMode, ampMode,
|
|
398
398
|
// piMode, jcodeMode, noTelemetry, jsonMode, helpMode, tierFilter }
|
|
399
399
|
//
|
|
@@ -446,11 +446,13 @@ export function parseArgs(argv) {
|
|
|
446
446
|
const fiableMode = flags.includes('--fiable')
|
|
447
447
|
const openCodeMode = flags.includes('--opencode')
|
|
448
448
|
const openCodeDesktopMode = flags.includes('--opencode-desktop')
|
|
449
|
+
const openCodeWebMode = flags.includes('--opencode-web')
|
|
449
450
|
const openClawMode = flags.includes('--openclaw')
|
|
450
451
|
const aiderMode = flags.includes('--aider')
|
|
451
452
|
const crushMode = flags.includes('--crush')
|
|
452
453
|
const gooseMode = flags.includes('--goose')
|
|
453
454
|
const qwenMode = flags.includes('--qwen')
|
|
455
|
+
const kiloMode = flags.includes('--kilo')
|
|
454
456
|
const openHandsMode = flags.includes('--openhands')
|
|
455
457
|
const ampMode = flags.includes('--amp')
|
|
456
458
|
const piMode = flags.includes('--pi')
|
|
@@ -492,11 +494,13 @@ export function parseArgs(argv) {
|
|
|
492
494
|
fiableMode,
|
|
493
495
|
openCodeMode,
|
|
494
496
|
openCodeDesktopMode,
|
|
497
|
+
openCodeWebMode,
|
|
495
498
|
openClawMode,
|
|
496
499
|
aiderMode,
|
|
497
500
|
crushMode,
|
|
498
501
|
gooseMode,
|
|
499
502
|
qwenMode,
|
|
503
|
+
kiloMode,
|
|
500
504
|
openHandsMode,
|
|
501
505
|
ampMode,
|
|
502
506
|
piMode,
|