free-coding-models 0.3.51 → 0.3.52
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 +6 -0
- package/README.md +4 -1
- package/package.json +1 -1
- package/src/app.js +24 -8
- package/src/command-palette.js +1 -0
- package/src/constants.js +10 -2
- package/src/key-handler.js +23 -1
- package/src/opencode.js +74 -0
- package/src/render-table.js +209 -127
- package/src/shell-env.js +3 -0
- package/src/tool-metadata.js +4 -2
- package/src/updater.js +2 -0
- package/src/utils.js +4 -2
- package/web/dist/assets/{index-CJjHD8Iz.js → index-D49esfAN.js} +1 -1
- package/web/dist/index.html +1 -1
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-metadata.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
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] },
|
|
@@ -49,7 +50,7 @@ export const TOOL_METADATA = {
|
|
|
49
50
|
// 📖 OpenCode CLI + Desktop are merged into a single 📦 slot since they share compatibility.
|
|
50
51
|
// 📖 Each slot maps to one or more toolKeys for compatibility checking.
|
|
51
52
|
export const COMPAT_COLUMN_SLOTS = [
|
|
52
|
-
{ emoji: '📦', toolKeys: ['opencode', 'opencode-desktop'], color: [110, 214, 255] },
|
|
53
|
+
{ emoji: '📦', toolKeys: ['opencode', 'opencode-desktop', 'opencode-web'], color: [110, 214, 255] },
|
|
53
54
|
{ emoji: '🦞', toolKeys: ['openclaw'], color: [255, 129, 129] },
|
|
54
55
|
{ emoji: '💘', toolKeys: ['crush'], color: [255, 168, 209] },
|
|
55
56
|
{ emoji: '🪿', toolKeys: ['goose'], color: [132, 235, 168] },
|
|
@@ -72,6 +73,7 @@ export const TOOL_MODE_ORDER = [
|
|
|
72
73
|
'pi',
|
|
73
74
|
'jcode',
|
|
74
75
|
'opencode-desktop',
|
|
76
|
+
'opencode-web',
|
|
75
77
|
'openclaw',
|
|
76
78
|
'crush',
|
|
77
79
|
'goose',
|
|
@@ -100,7 +102,7 @@ export function getToolModeOrder() {
|
|
|
100
102
|
const REGULAR_TOOLS = Object.keys(TOOL_METADATA).filter(k => !TOOL_METADATA[k].cliOnly)
|
|
101
103
|
|
|
102
104
|
// 📖 Zen-only tools: OpenCode Zen models can ONLY run on OpenCode CLI / OpenCode Desktop.
|
|
103
|
-
const ZEN_COMPATIBLE_TOOLS = ['opencode', 'opencode-desktop']
|
|
105
|
+
const ZEN_COMPATIBLE_TOOLS = ['opencode', 'opencode-desktop', 'opencode-web']
|
|
104
106
|
|
|
105
107
|
/**
|
|
106
108
|
* 📖 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,
|
|
390
|
+
// - Boolean flags: --best, --fiable, --opencode, --opencode-desktop, --opencode-web, --openclaw,
|
|
391
391
|
// --aider, --crush, --goose, --qwen,
|
|
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,6 +446,7 @@ 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')
|
|
@@ -492,6 +493,7 @@ export function parseArgs(argv) {
|
|
|
492
493
|
fiableMode,
|
|
493
494
|
openCodeMode,
|
|
494
495
|
openCodeDesktopMode,
|
|
496
|
+
openCodeWebMode,
|
|
495
497
|
openClawMode,
|
|
496
498
|
aiderMode,
|
|
497
499
|
crushMode,
|