free-coding-models 0.3.23 → 0.3.25
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 +41 -0
- package/README.md +72 -17
- package/package.json +1 -1
- package/sources.js +60 -0
- package/src/app.js +97 -6
- package/src/command-palette.js +3 -1
- package/src/constants.js +5 -2
- package/src/endpoint-installer.js +2 -1
- package/src/key-handler.js +607 -5
- package/src/mouse.js +186 -0
- package/src/overlays.js +159 -2
- package/src/provider-metadata.js +25 -0
- package/src/render-helpers.js +1 -1
- package/src/render-table.js +181 -8
- package/src/theme.js +6 -0
- package/src/tool-bootstrap.js +22 -0
- package/src/tool-launchers.js +93 -2
- package/src/tool-metadata.js +94 -11
- package/src/utils.js +4 -0
package/src/render-table.js
CHANGED
|
@@ -50,12 +50,47 @@ import { TIER_COLOR } from './tier-colors.js'
|
|
|
50
50
|
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
|
|
51
51
|
import { usagePlaceholderForProvider } from './ping.js'
|
|
52
52
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
|
|
53
|
-
import { getToolMeta } from './tool-metadata.js'
|
|
53
|
+
import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER, isModelCompatibleWithTool } from './tool-metadata.js'
|
|
54
54
|
import { getColumnSpacing } from './ui-config.js'
|
|
55
55
|
|
|
56
56
|
const require = createRequire(import.meta.url)
|
|
57
57
|
const { version: LOCAL_VERSION } = require('../package.json')
|
|
58
58
|
|
|
59
|
+
// 📖 Mouse support: column boundary map updated every frame by renderTable().
|
|
60
|
+
// 📖 Each entry maps a column name to its display X-start and X-end (1-based, inclusive).
|
|
61
|
+
// 📖 headerRow is the 1-based terminal row of the column header line.
|
|
62
|
+
// 📖 firstModelRow/lastModelRow are the 1-based terminal rows of the first/last visible model row.
|
|
63
|
+
// 📖 Exported so the mouse handler can translate click coordinates into column/row targets.
|
|
64
|
+
let _lastLayout = {
|
|
65
|
+
columns: [], // 📖 Array of { name, xStart, xEnd } in display order
|
|
66
|
+
headerRow: 0, // 📖 1-based terminal row of the column headers
|
|
67
|
+
firstModelRow: 0, // 📖 1-based terminal row of the first visible model
|
|
68
|
+
lastModelRow: 0, // 📖 1-based terminal row of the last visible model
|
|
69
|
+
viewportStartIdx: 0, // 📖 index into sorted[] of the first visible model
|
|
70
|
+
viewportEndIdx: 0, // 📖 index into sorted[] past the last visible model
|
|
71
|
+
hasAboveIndicator: false, // 📖 whether "... N more above ..." is shown
|
|
72
|
+
hasBelowIndicator: false, // 📖 whether "... N more below ..." is shown
|
|
73
|
+
footerHotkeys: [], // 📖 Array of { key, row, xStart, xEnd } for footer click zones
|
|
74
|
+
}
|
|
75
|
+
export function getLastLayout() { return _lastLayout }
|
|
76
|
+
|
|
77
|
+
// 📖 Column name → sort key mapping for mouse click-to-sort on header row
|
|
78
|
+
const COLUMN_SORT_MAP = {
|
|
79
|
+
rank: 'rank',
|
|
80
|
+
tier: null, // 📖 Tier column click cycles tier filter rather than sorting
|
|
81
|
+
swe: 'swe',
|
|
82
|
+
ctx: 'ctx',
|
|
83
|
+
model: 'model',
|
|
84
|
+
source: 'origin',
|
|
85
|
+
ping: 'ping',
|
|
86
|
+
avg: 'avg',
|
|
87
|
+
health: 'condition',
|
|
88
|
+
verdict: 'verdict',
|
|
89
|
+
stability: 'stability',
|
|
90
|
+
uptime: 'uptime',
|
|
91
|
+
}
|
|
92
|
+
export { COLUMN_SORT_MAP }
|
|
93
|
+
|
|
59
94
|
// 📖 Provider column palette: soft pastel rainbow so each provider stays easy
|
|
60
95
|
// 📖 to spot without turning the table into a harsh neon wall.
|
|
61
96
|
// 📖 Exported for use in overlays (settings screen) and logs.
|
|
@@ -109,9 +144,12 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
109
144
|
|
|
110
145
|
// 📖 Tool badge keeps the active launch target visible in the header, so the
|
|
111
146
|
// 📖 footer no longer needs a redundant Enter action or mode toggle reminder.
|
|
147
|
+
// 📖 Tool name is colored with its unique tool color for quick recognition.
|
|
112
148
|
const toolMeta = getToolMeta(mode)
|
|
113
149
|
const toolBadgeColor = mode === 'openclaw' ? themeColors.warningBold : themeColors.accentBold
|
|
114
|
-
const
|
|
150
|
+
const toolColor = toolMeta.color ? chalk.rgb(...toolMeta.color) : toolBadgeColor
|
|
151
|
+
const modeBadge = toolBadgeColor(' [ ') + themeColors.hotkey('Z') + toolBadgeColor(' Tool : ') + toolColor.bold(`${toolMeta.emoji} ${toolMeta.label}`) + toolBadgeColor(' ]')
|
|
152
|
+
|
|
115
153
|
const activeHeaderBadge = (text, bg) => themeColors.badge(text, bg, getReadableTextRgb(bg))
|
|
116
154
|
const versionStatus = getVersionStatusInfo(settingsUpdateState, settingsUpdateLatestVersion, startupLatestVersion, versionAlertsEnabled)
|
|
117
155
|
|
|
@@ -157,6 +195,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
157
195
|
const W_STATUS = 18
|
|
158
196
|
const W_VERDICT = 14
|
|
159
197
|
const W_UPTIME = 6
|
|
198
|
+
|
|
160
199
|
// const W_TOKENS = 7 // Used column removed
|
|
161
200
|
// const W_USAGE = 7 // Usage column removed
|
|
162
201
|
const MIN_TABLE_WIDTH = WIDTH_WARNING_MIN_COLS
|
|
@@ -205,10 +244,38 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
205
244
|
if (calcWidth() > terminalCols) showTier = false
|
|
206
245
|
if (calcWidth() > terminalCols) showStability = false
|
|
207
246
|
}
|
|
247
|
+
|
|
248
|
+
// 📖 Mouse support: compute column boundaries from the resolved responsive widths.
|
|
249
|
+
// 📖 This builds an ordered array of { name, xStart, xEnd } (1-based display columns)
|
|
250
|
+
// 📖 matching exactly what renderTable paints so click-to-sort hits the right column.
|
|
251
|
+
{
|
|
252
|
+
const colDefs = []
|
|
253
|
+
if (showRank) colDefs.push({ name: 'rank', width: W_RANK })
|
|
254
|
+
if (showTier) colDefs.push({ name: 'tier', width: W_TIER })
|
|
255
|
+
colDefs.push({ name: 'swe', width: W_SWE })
|
|
256
|
+
colDefs.push({ name: 'ctx', width: W_CTX })
|
|
257
|
+
colDefs.push({ name: 'model', width: W_MODEL })
|
|
258
|
+
colDefs.push({ name: 'source', width: wSource })
|
|
259
|
+
colDefs.push({ name: 'ping', width: wPing })
|
|
260
|
+
colDefs.push({ name: 'avg', width: wAvg })
|
|
261
|
+
colDefs.push({ name: 'health', width: wStatus })
|
|
262
|
+
colDefs.push({ name: 'verdict', width: W_VERDICT })
|
|
263
|
+
if (showStability) colDefs.push({ name: 'stability', width: wStab })
|
|
264
|
+
if (showUptime) colDefs.push({ name: 'uptime', width: W_UPTIME })
|
|
265
|
+
let x = ROW_MARGIN + 1 // 📖 1-based: first column starts after the 2-char left margin
|
|
266
|
+
const columns = []
|
|
267
|
+
for (let i = 0; i < colDefs.length; i++) {
|
|
268
|
+
const { name, width } = colDefs[i]
|
|
269
|
+
const xEnd = x + width - 1
|
|
270
|
+
columns.push({ name, xStart: x, xEnd })
|
|
271
|
+
x = xEnd + 1 + SEP_W // 📖 skip past the ' │ ' separator
|
|
272
|
+
}
|
|
273
|
+
_lastLayout.columns = columns
|
|
274
|
+
}
|
|
208
275
|
const warningDurationMs = 2_000
|
|
209
276
|
const elapsed = widthWarningStartedAt ? Math.max(0, Date.now() - widthWarningStartedAt) : warningDurationMs
|
|
210
277
|
const remainingMs = Math.max(0, warningDurationMs - elapsed)
|
|
211
|
-
const showWidthWarning = terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH && !widthWarningDismissed &&
|
|
278
|
+
const showWidthWarning = terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH && !widthWarningDismissed && remainingMs > 0
|
|
212
279
|
|
|
213
280
|
if (showWidthWarning) {
|
|
214
281
|
const lines = []
|
|
@@ -261,8 +328,8 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
261
328
|
const tierH = 'Tier'
|
|
262
329
|
const originH = 'Provider'
|
|
263
330
|
const modelH = 'Model'
|
|
264
|
-
const sweH = sortColumn === 'swe' ? dir + '
|
|
265
|
-
const ctxH = sortColumn === 'ctx' ? dir + '
|
|
331
|
+
const sweH = sortColumn === 'swe' ? (dir + 'SWE%') : 'SWE%'
|
|
332
|
+
const ctxH = sortColumn === 'ctx' ? (dir + 'CTX') : 'CTX'
|
|
266
333
|
// 📖 Compact labels: 'Lat. P' / 'Avg. P' / 'StaB.' to save horizontal space
|
|
267
334
|
const pingLabel = isCompact ? 'Lat. P' : 'Latest Ping'
|
|
268
335
|
const avgLabel = isCompact ? 'Avg. P' : 'Avg Ping'
|
|
@@ -271,8 +338,10 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
271
338
|
const avgH = sortColumn === 'avg' ? dir + ' ' + avgLabel : avgLabel
|
|
272
339
|
const healthH = sortColumn === 'condition' ? dir + ' Health' : 'Health'
|
|
273
340
|
const verdictH = sortColumn === 'verdict' ? dir + ' Verdict' : 'Verdict'
|
|
274
|
-
|
|
275
|
-
|
|
341
|
+
// 📖 Stability: in non-compact the arrow eats 2 chars ('↑ '), so truncate to fit wStab.
|
|
342
|
+
// 📖 Compact is fine because '↑ StaB.' (7) < wStab (8).
|
|
343
|
+
const stabH = sortColumn === 'stability' ? (dir + (isCompact ? ' ' + stabLabel : 'Stability')) : stabLabel
|
|
344
|
+
const uptimeH = sortColumn === 'uptime' ? (dir + 'Up%') : 'Up%'
|
|
276
345
|
|
|
277
346
|
// 📖 Helper to colorize first letter for keyboard shortcuts
|
|
278
347
|
// 📖 IMPORTANT: Pad PLAIN TEXT first, then apply colors to avoid alignment issues
|
|
@@ -319,6 +388,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
319
388
|
const padding = ' '.repeat(Math.max(0, W_UPTIME - plain.length))
|
|
320
389
|
return themeColors.hotkey('U') + themeColors.dim('p%' + padding)
|
|
321
390
|
})()
|
|
391
|
+
|
|
322
392
|
// 📖 Usage column removed from UI – no header or separator for it.
|
|
323
393
|
// 📖 Header row: conditionally include columns based on responsive visibility
|
|
324
394
|
const headerParts = []
|
|
@@ -329,6 +399,10 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
329
399
|
if (showUptime) headerParts.push(uptimeH_c)
|
|
330
400
|
lines.push(' ' + headerParts.join(COL_SEP))
|
|
331
401
|
|
|
402
|
+
// 📖 Mouse support: the column header row is the last line we just pushed.
|
|
403
|
+
// 📖 Terminal rows are 1-based, so line index (lines.length-1) → terminal row lines.length.
|
|
404
|
+
_lastLayout.headerRow = lines.length
|
|
405
|
+
|
|
332
406
|
|
|
333
407
|
|
|
334
408
|
if (sorted.length === 0) {
|
|
@@ -360,6 +434,14 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
360
434
|
lines.push(themeColors.dim(` ... ${vp.startIdx} more above ...`))
|
|
361
435
|
}
|
|
362
436
|
|
|
437
|
+
// 📖 Mouse support: record where model rows begin in the terminal (1-based).
|
|
438
|
+
// 📖 The next line pushed will be the first visible model row.
|
|
439
|
+
const _firstModelLineIdx = lines.length // 📖 0-based index into lines[]
|
|
440
|
+
_lastLayout.viewportStartIdx = vp.startIdx
|
|
441
|
+
_lastLayout.viewportEndIdx = vp.endIdx
|
|
442
|
+
_lastLayout.hasAboveIndicator = vp.hasAbove
|
|
443
|
+
_lastLayout.hasBelowIndicator = vp.hasBelow
|
|
444
|
+
|
|
363
445
|
for (let i = vp.startIdx; i < vp.endIdx; i++) {
|
|
364
446
|
const r = sorted[i]
|
|
365
447
|
const tierFn = TIER_COLOR[r.tier] ?? ((text) => themeColors.text(text))
|
|
@@ -585,6 +667,9 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
585
667
|
const sourceCursorText = providerDisplay.padEnd(wSource)
|
|
586
668
|
const sourceCell = isCursor ? themeColors.provider(r.providerKey, sourceCursorText, { bold: true }) : source
|
|
587
669
|
|
|
670
|
+
// 📖 Check if this model is incompatible with the active tool mode
|
|
671
|
+
const isIncompatible = !isModelCompatibleWithTool(r.providerKey, mode)
|
|
672
|
+
|
|
588
673
|
// 📖 Usage column removed from UI – no usage data displayed.
|
|
589
674
|
// (We keep the logic but do not render it.)
|
|
590
675
|
const usageCell = ''
|
|
@@ -600,6 +685,10 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
600
685
|
|
|
601
686
|
if (isCursor) {
|
|
602
687
|
lines.push(themeColors.bgModelCursor(row))
|
|
688
|
+
} else if (isIncompatible) {
|
|
689
|
+
// 📖 Dark red background for models incompatible with the active tool mode.
|
|
690
|
+
// 📖 This visually warns the user that selecting this model won't work with their current tool.
|
|
691
|
+
lines.push(chalk.bgRgb(60, 15, 15).rgb(180, 130, 130)(row))
|
|
603
692
|
} else if (r.isRecommended) {
|
|
604
693
|
// 📖 Medium green background for recommended models (distinguishable from favorites)
|
|
605
694
|
lines.push(themeColors.bgModelRecommended(row))
|
|
@@ -610,6 +699,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
610
699
|
}
|
|
611
700
|
}
|
|
612
701
|
|
|
702
|
+
// 📖 Mouse support: record the 1-based terminal row range of model data rows.
|
|
703
|
+
// 📖 _firstModelLineIdx was captured before the loop; lines.length is now past the last model row.
|
|
704
|
+
_lastLayout.firstModelRow = _firstModelLineIdx + 1 // 📖 convert 0-based line index → 1-based terminal row
|
|
705
|
+
_lastLayout.lastModelRow = lines.length // 📖 last pushed line is at lines.length (1-based)
|
|
706
|
+
|
|
613
707
|
if (vp.hasBelow) {
|
|
614
708
|
lines.push(themeColors.dim(` ... ${sorted.length - vp.endIdx} more below ...`))
|
|
615
709
|
}
|
|
@@ -624,7 +718,40 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
624
718
|
const activeHotkey = (keyLabel, text, bg) => themeColors.badge(`${keyLabel}${text}`, bg, getReadableTextRgb(bg))
|
|
625
719
|
const favoritesModeBg = favoritesPinnedAndSticky ? [157, 122, 48] : [95, 95, 95]
|
|
626
720
|
const favoritesModeLabel = favoritesPinnedAndSticky ? ' Favorites Pinned' : ' Favorites Normal'
|
|
721
|
+
|
|
722
|
+
// 📖 Mouse support: build footer hotkey zones alongside the footer lines.
|
|
723
|
+
// 📖 Each zone records { key, row (1-based terminal row), xStart, xEnd (1-based display cols) }.
|
|
724
|
+
// 📖 We accumulate display position as we build each footer line's parts.
|
|
725
|
+
const footerHotkeys = []
|
|
726
|
+
|
|
627
727
|
// 📖 Line 1: core navigation + filtering shortcuts
|
|
728
|
+
// 📖 Build as parts array so we can compute click zones and still join for display.
|
|
729
|
+
{
|
|
730
|
+
const parts = [
|
|
731
|
+
{ text: ' ', key: null },
|
|
732
|
+
{ text: 'F Toggle Favorite', key: 'f' },
|
|
733
|
+
{ text: ' • ', key: null },
|
|
734
|
+
{ text: 'Y' + favoritesModeLabel, key: 'y' },
|
|
735
|
+
{ text: ' • ', key: null },
|
|
736
|
+
{ text: tierFilterMode > 0 ? `T Tier (${activeTierLabel})` : 'T Tier', key: 't' },
|
|
737
|
+
{ text: ' • ', key: null },
|
|
738
|
+
{ text: originFilterMode > 0 ? `D Provider (${activeOriginLabel})` : 'D Provider', key: 'd' },
|
|
739
|
+
{ text: ' • ', key: null },
|
|
740
|
+
{ text: 'E Show only configured models', key: 'e' },
|
|
741
|
+
{ text: ' • ', key: null },
|
|
742
|
+
{ text: 'P Settings', key: 'p' },
|
|
743
|
+
{ text: ' • ', key: null },
|
|
744
|
+
{ text: 'K Help', key: 'k' },
|
|
745
|
+
]
|
|
746
|
+
const footerRow1 = lines.length + 1 // 📖 1-based terminal row (line hasn't been pushed yet)
|
|
747
|
+
let xPos = 1
|
|
748
|
+
for (const part of parts) {
|
|
749
|
+
const w = displayWidth(part.text)
|
|
750
|
+
if (part.key) footerHotkeys.push({ key: part.key, row: footerRow1, xStart: xPos, xEnd: xPos + w - 1 })
|
|
751
|
+
xPos += w
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
628
755
|
lines.push(
|
|
629
756
|
' ' + hotkey('F', ' Toggle Favorite') +
|
|
630
757
|
themeColors.dim(` • `) +
|
|
@@ -638,12 +765,35 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
638
765
|
? activeHotkey('D', ` Provider (${activeOriginLabel})`, PROVIDER_COLOR[[null, ...Object.keys(sources)][originFilterMode]] || [255, 255, 255])
|
|
639
766
|
: hotkey('D', ' Provider')) +
|
|
640
767
|
themeColors.dim(` • `) +
|
|
641
|
-
(hideUnconfiguredModels ? activeHotkey('E', '
|
|
768
|
+
(hideUnconfiguredModels ? activeHotkey('E', ' Show only configured models', configuredBadgeBg) : hotkey('E', ' Show only configured models')) +
|
|
642
769
|
themeColors.dim(` • `) +
|
|
643
770
|
hotkey('P', ' Settings') +
|
|
644
771
|
themeColors.dim(` • `) +
|
|
645
772
|
hotkey('K', ' Help')
|
|
646
773
|
)
|
|
774
|
+
|
|
775
|
+
// 📖 Line 2: command palette, recommend, feedback, theme
|
|
776
|
+
{
|
|
777
|
+
const cpText = ' NEW ! CTRL+P ⚡️ Command Palette '
|
|
778
|
+
const parts = [
|
|
779
|
+
{ text: ' ', key: null },
|
|
780
|
+
{ text: cpText, key: 'ctrl+p' },
|
|
781
|
+
{ text: ' • ', key: null },
|
|
782
|
+
{ text: 'Q Smart Recommend', key: 'q' },
|
|
783
|
+
{ text: ' • ', key: null },
|
|
784
|
+
{ text: 'G Theme', key: 'g' },
|
|
785
|
+
{ text: ' • ', key: null },
|
|
786
|
+
{ text: 'I Feedback, bugs & requests', key: 'i' },
|
|
787
|
+
]
|
|
788
|
+
const footerRow2 = lines.length + 1
|
|
789
|
+
let xPos = 1
|
|
790
|
+
for (const part of parts) {
|
|
791
|
+
const w = displayWidth(part.text)
|
|
792
|
+
if (part.key) footerHotkeys.push({ key: part.key, row: footerRow2, xStart: xPos, xEnd: xPos + w - 1 })
|
|
793
|
+
xPos += w
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
647
797
|
// 📖 Line 2: command palette (highlighted as new), recommend, feedback, and extended hints.
|
|
648
798
|
// 📖 CTRL+P ⚡️ Command Palette uses neon-green-on-dark-green background to highlight the feature.
|
|
649
799
|
const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold(' NEW ! CTRL+P ⚡️ Command Palette ')
|
|
@@ -699,6 +849,29 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
699
849
|
filterBadge = chalk.bgYellow.black.bold(` ${filterPrefix}${visibleFilter}${filterSuffix} `)
|
|
700
850
|
}
|
|
701
851
|
|
|
852
|
+
// 📖 Mouse support: track last footer line hotkey zones
|
|
853
|
+
{
|
|
854
|
+
const lastFooterRow = lines.length + 1 // 📖 1-based terminal row (line about to be pushed)
|
|
855
|
+
const parts = [
|
|
856
|
+
{ text: ' ', key: null },
|
|
857
|
+
{ text: 'N Changelog', key: 'n' },
|
|
858
|
+
]
|
|
859
|
+
if (hasCustomFilter) {
|
|
860
|
+
parts.push({ text: ' • ', key: null })
|
|
861
|
+
// 📖 X key clears filter — compute width from rendered badge text
|
|
862
|
+
const badgePlain = `X Disable filter: "${customTextFilter.trim().replace(/\s+/g, ' ')}"`
|
|
863
|
+
parts.push({ text: ` ${badgePlain} `, key: 'x' })
|
|
864
|
+
}
|
|
865
|
+
let xPos = 1
|
|
866
|
+
for (const part of parts) {
|
|
867
|
+
const w = displayWidth(part.text)
|
|
868
|
+
if (part.key) footerHotkeys.push({ key: part.key, row: lastFooterRow, xStart: xPos, xEnd: xPos + w - 1 })
|
|
869
|
+
xPos += w
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
_lastLayout.footerHotkeys = footerHotkeys
|
|
874
|
+
|
|
702
875
|
lines.push(
|
|
703
876
|
' ' + themeColors.hotkey('N') + themeColors.dim(' Changelog') +
|
|
704
877
|
(filterBadge
|
package/src/theme.js
CHANGED
|
@@ -141,6 +141,9 @@ const PROVIDER_PALETTES = {
|
|
|
141
141
|
qwen: [255, 213, 128],
|
|
142
142
|
zai: [150, 208, 255],
|
|
143
143
|
iflow: [211, 229, 101],
|
|
144
|
+
rovo: [148, 163, 184],
|
|
145
|
+
gemini: [66, 165, 245],
|
|
146
|
+
'opencode-zen': [185, 146, 255],
|
|
144
147
|
},
|
|
145
148
|
light: {
|
|
146
149
|
nvidia: [0, 126, 73],
|
|
@@ -163,6 +166,9 @@ const PROVIDER_PALETTES = {
|
|
|
163
166
|
qwen: [132, 89, 0],
|
|
164
167
|
zai: [0, 104, 171],
|
|
165
168
|
iflow: [107, 130, 0],
|
|
169
|
+
rovo: [90, 100, 126],
|
|
170
|
+
gemini: [15, 97, 175],
|
|
171
|
+
'opencode-zen': [108, 58, 183],
|
|
166
172
|
},
|
|
167
173
|
}
|
|
168
174
|
|
package/src/tool-bootstrap.js
CHANGED
|
@@ -217,6 +217,28 @@ export const TOOL_BOOTSTRAP_METADATA = {
|
|
|
217
217
|
},
|
|
218
218
|
},
|
|
219
219
|
},
|
|
220
|
+
rovo: {
|
|
221
|
+
binary: 'acli',
|
|
222
|
+
docsUrl: 'https://support.atlassian.com/rovo/docs/install-and-run-rovo-dev-cli-on-your-device/',
|
|
223
|
+
install: {
|
|
224
|
+
default: {
|
|
225
|
+
shellCommand: 'npm install -g acli',
|
|
226
|
+
summary: 'Rovo Dev CLI requires ACLI installation. Visit the documentation for platform-specific instructions.',
|
|
227
|
+
note: 'Rovo is an Atlassian tool that requires an Atlassian account with Rovo Dev activated.',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
gemini: {
|
|
232
|
+
binary: 'gemini',
|
|
233
|
+
docsUrl: 'https://github.com/google-gemini/gemini-cli',
|
|
234
|
+
install: {
|
|
235
|
+
default: {
|
|
236
|
+
shellCommand: 'npm install -g @google/gemini-cli',
|
|
237
|
+
summary: 'Install Gemini CLI globally via npm.',
|
|
238
|
+
note: 'After installation, run `gemini` to authenticate with your Google account.',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
220
242
|
}
|
|
221
243
|
|
|
222
244
|
export function getToolBootstrapMeta(mode) {
|
package/src/tool-launchers.js
CHANGED
|
@@ -41,7 +41,7 @@ import { sources } from '../sources.js'
|
|
|
41
41
|
import { PROVIDER_COLOR } from './render-table.js'
|
|
42
42
|
import { getApiKey } from './config.js'
|
|
43
43
|
import { ENV_VAR_NAMES, isWindows } from './provider-metadata.js'
|
|
44
|
-
import { getToolMeta } from './tool-metadata.js'
|
|
44
|
+
import { getToolMeta, TOOL_METADATA } from './tool-metadata.js'
|
|
45
45
|
import { PROVIDER_METADATA } from './provider-metadata.js'
|
|
46
46
|
import { resolveToolBinaryPath } from './tool-bootstrap.js'
|
|
47
47
|
|
|
@@ -378,6 +378,57 @@ function writeOpenHandsEnv(model, apiKey, baseUrl, paths = getDefaultToolPaths()
|
|
|
378
378
|
return { filePath, backupPath }
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
/**
|
|
382
|
+
* 📖 writeRovoConfig - Configure Rovo Dev CLI model selection
|
|
383
|
+
*
|
|
384
|
+
* Rovo Dev CLI uses ~/.rovodev/config.yml for configuration.
|
|
385
|
+
* We write the model ID to the config file before launching.
|
|
386
|
+
*
|
|
387
|
+
* @param {Object} model - Selected model with modelId
|
|
388
|
+
* @param {string} configPath - Path to Rovo config file
|
|
389
|
+
* @returns {{ filePath: string, backupPath: string | null }}
|
|
390
|
+
*/
|
|
391
|
+
function writeRovoConfig(model, configPath = join(homedir(), '.rovodev', 'config.yml')) {
|
|
392
|
+
const backupPath = backupIfExists(configPath)
|
|
393
|
+
const config = {
|
|
394
|
+
agent: {
|
|
395
|
+
modelId: model.modelId,
|
|
396
|
+
},
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
ensureDir(configPath)
|
|
400
|
+
writeFileSync(configPath, `agent:\n modelId: "${model.modelId}"\n`)
|
|
401
|
+
return { filePath: configPath, backupPath }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 📖 buildGeminiEnv - Build environment variables for Gemini CLI
|
|
406
|
+
*
|
|
407
|
+
* Gemini CLI supports OpenAI-compatible APIs via environment variables:
|
|
408
|
+
* - GEMINI_API_BASE_URL: Custom API endpoint
|
|
409
|
+
* - GEMINI_API_KEY: API key for custom endpoint
|
|
410
|
+
*
|
|
411
|
+
* @param {Object} model - Selected model with providerKey
|
|
412
|
+
* @param {Object} config - Full app config
|
|
413
|
+
* @param {Object} options - Env options
|
|
414
|
+
* @returns {NodeJS.ProcessEnv}
|
|
415
|
+
*/
|
|
416
|
+
function buildGeminiEnv(model, config, options = {}) {
|
|
417
|
+
const providerKey = model.providerKey || 'gemini'
|
|
418
|
+
const apiKey = getApiKey(config, providerKey)
|
|
419
|
+
const baseUrl = getProviderBaseUrl(providerKey)
|
|
420
|
+
|
|
421
|
+
const env = cloneInheritedEnv(process.env, SANITIZED_TOOL_ENV_KEYS)
|
|
422
|
+
|
|
423
|
+
// If we have a custom API key and base URL, configure OpenAI-compatible mode
|
|
424
|
+
if (apiKey && baseUrl && options.includeProviderEnv) {
|
|
425
|
+
env.GEMINI_API_BASE_URL = baseUrl
|
|
426
|
+
env.GEMINI_API_KEY = apiKey
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return env
|
|
430
|
+
}
|
|
431
|
+
|
|
381
432
|
function printConfigArtifacts(toolName, artifacts = []) {
|
|
382
433
|
for (const artifact of artifacts) {
|
|
383
434
|
if (!artifact?.path) continue
|
|
@@ -420,7 +471,9 @@ export function prepareExternalToolLaunch(mode, model, config, options = {}) {
|
|
|
420
471
|
inheritedEnv: options.inheritedEnv,
|
|
421
472
|
})
|
|
422
473
|
|
|
423
|
-
|
|
474
|
+
const isCliOnlyTool = TOOL_METADATA[mode]?.cliOnly === true
|
|
475
|
+
|
|
476
|
+
if (!apiKey && mode !== 'amp' && !isCliOnlyTool) {
|
|
424
477
|
const providerRgb = PROVIDER_COLOR[model.providerKey] ?? [105, 190, 245]
|
|
425
478
|
const providerName = sources[model.providerKey]?.name || model.providerKey
|
|
426
479
|
const coloredProviderName = chalk.bold.rgb(...providerRgb)(providerName)
|
|
@@ -544,6 +597,34 @@ export function prepareExternalToolLaunch(mode, model, config, options = {}) {
|
|
|
544
597
|
}
|
|
545
598
|
}
|
|
546
599
|
|
|
600
|
+
if (mode === 'rovo') {
|
|
601
|
+
const result = writeRovoConfig(model, join(homedir(), '.rovodev', 'config.yml'), paths)
|
|
602
|
+
console.log(chalk.dim(` 📖 Rovo Dev CLI configured with model: ${model.modelId}`))
|
|
603
|
+
return {
|
|
604
|
+
command: 'acli',
|
|
605
|
+
args: ['rovodev', 'run'],
|
|
606
|
+
env,
|
|
607
|
+
apiKey: null,
|
|
608
|
+
baseUrl: null,
|
|
609
|
+
meta,
|
|
610
|
+
configArtifacts: [{ path: result.filePath, backupPath: result.backupPath, label: 'config' }],
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (mode === 'gemini') {
|
|
615
|
+
const geminiEnv = buildGeminiEnv(model, config, { includeProviderEnv: options.includeProviderEnv })
|
|
616
|
+
console.log(chalk.dim(` 📖 Gemini CLI will use model: ${model.modelId}`))
|
|
617
|
+
return {
|
|
618
|
+
command: 'gemini',
|
|
619
|
+
args: [],
|
|
620
|
+
env: { ...env, ...geminiEnv },
|
|
621
|
+
apiKey: geminiEnv.GEMINI_API_KEY || null,
|
|
622
|
+
baseUrl: geminiEnv.GEMINI_API_BASE_URL || null,
|
|
623
|
+
meta,
|
|
624
|
+
configArtifacts: [],
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
547
628
|
return {
|
|
548
629
|
blocked: true,
|
|
549
630
|
exitCode: 1,
|
|
@@ -598,6 +679,16 @@ export async function startExternalTool(mode, model, config) {
|
|
|
598
679
|
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
599
680
|
}
|
|
600
681
|
|
|
682
|
+
if (mode === 'rovo') {
|
|
683
|
+
console.log(chalk.dim(` 📖 Launching Rovo Dev CLI in interactive mode...`))
|
|
684
|
+
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (mode === 'gemini') {
|
|
688
|
+
console.log(chalk.dim(` 📖 Launching Gemini CLI...`))
|
|
689
|
+
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
690
|
+
}
|
|
691
|
+
|
|
601
692
|
console.log(chalk.red(` X Unsupported external tool mode: ${mode}`))
|
|
602
693
|
return 1
|
|
603
694
|
}
|
package/src/tool-metadata.js
CHANGED
|
@@ -19,21 +19,44 @@
|
|
|
19
19
|
* → `getToolMeta` — return display metadata for one mode
|
|
20
20
|
* → `getToolModeOrder` — stable mode cycle order for the `Z` hotkey
|
|
21
21
|
*
|
|
22
|
-
* @exports TOOL_METADATA, TOOL_MODE_ORDER, getToolMeta, getToolModeOrder
|
|
22
|
+
* @exports TOOL_METADATA, TOOL_MODE_ORDER, COMPAT_COLUMN_SLOTS, getToolMeta, getToolModeOrder
|
|
23
23
|
*/
|
|
24
|
+
// 📖 Each tool has a unique `color` RGB tuple used for the "Compatible with" column
|
|
25
|
+
// 📖 and for coloring the tool name in the Z cycle header badge.
|
|
26
|
+
// 📖 `emoji` is the unique icon shown everywhere (header badge, compat column, palette, overlays).
|
|
27
|
+
// 📖 OpenCode CLI and Desktop share 📦 — they are the same platform, split only for launch logic.
|
|
24
28
|
export const TOOL_METADATA = {
|
|
25
|
-
opencode:
|
|
26
|
-
'opencode-desktop': { label: 'OpenCode Desktop', emoji: '
|
|
27
|
-
openclaw:
|
|
28
|
-
crush:
|
|
29
|
-
goose:
|
|
30
|
-
pi:
|
|
31
|
-
aider:
|
|
32
|
-
qwen:
|
|
33
|
-
openhands:
|
|
34
|
-
amp:
|
|
29
|
+
opencode: { label: 'OpenCode CLI', emoji: '📦', flag: '--opencode', color: [110, 214, 255] },
|
|
30
|
+
'opencode-desktop': { label: 'OpenCode Desktop', emoji: '📦', flag: '--opencode-desktop', color: [149, 205, 255] },
|
|
31
|
+
openclaw: { label: 'OpenClaw', emoji: '🦞', flag: '--openclaw', color: [255, 129, 129] },
|
|
32
|
+
crush: { label: 'Crush', emoji: '💘', flag: '--crush', color: [255, 168, 209] },
|
|
33
|
+
goose: { label: 'Goose', emoji: '🪿', flag: '--goose', color: [132, 235, 168] },
|
|
34
|
+
pi: { label: 'Pi', emoji: 'π', flag: '--pi', color: [173, 216, 230] },
|
|
35
|
+
aider: { label: 'Aider', emoji: '🛠', flag: '--aider', color: [255, 208, 102] },
|
|
36
|
+
qwen: { label: 'Qwen Code', emoji: '🐉', flag: '--qwen', color: [255, 213, 128] },
|
|
37
|
+
openhands: { label: 'OpenHands', emoji: '🤲', flag: '--openhands', color: [228, 191, 239] },
|
|
38
|
+
amp: { label: 'Amp', emoji: '⚡', flag: '--amp', color: [255, 232, 98] },
|
|
39
|
+
rovo: { label: 'Rovo Dev CLI', emoji: '🦘', flag: '--rovo', color: [148, 163, 184], cliOnly: true },
|
|
40
|
+
gemini: { label: 'Gemini CLI', emoji: '♊', flag: '--gemini', color: [66, 165, 245], cliOnly: true },
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
// 📖 Deduplicated emoji order for the "Compatible with" column.
|
|
44
|
+
// 📖 OpenCode CLI + Desktop are merged into a single 📦 slot since they share compatibility.
|
|
45
|
+
// 📖 Each slot maps to one or more toolKeys for compatibility checking.
|
|
46
|
+
export const COMPAT_COLUMN_SLOTS = [
|
|
47
|
+
{ emoji: '📦', toolKeys: ['opencode', 'opencode-desktop'], color: [110, 214, 255] },
|
|
48
|
+
{ emoji: '🦞', toolKeys: ['openclaw'], color: [255, 129, 129] },
|
|
49
|
+
{ emoji: '💘', toolKeys: ['crush'], color: [255, 168, 209] },
|
|
50
|
+
{ emoji: '🪿', toolKeys: ['goose'], color: [132, 235, 168] },
|
|
51
|
+
{ emoji: 'π', toolKeys: ['pi'], color: [173, 216, 230] },
|
|
52
|
+
{ emoji: '🛠', toolKeys: ['aider'], color: [255, 208, 102] },
|
|
53
|
+
{ emoji: '🐉', toolKeys: ['qwen'], color: [255, 213, 128] },
|
|
54
|
+
{ emoji: '🤲', toolKeys: ['openhands'], color: [228, 191, 239] },
|
|
55
|
+
{ emoji: '⚡', toolKeys: ['amp'], color: [255, 232, 98] },
|
|
56
|
+
{ emoji: '🦘', toolKeys: ['rovo'], color: [148, 163, 184] },
|
|
57
|
+
{ emoji: '♊', toolKeys: ['gemini'], color: [66, 165, 245] },
|
|
58
|
+
]
|
|
59
|
+
|
|
37
60
|
export const TOOL_MODE_ORDER = [
|
|
38
61
|
'opencode',
|
|
39
62
|
'opencode-desktop',
|
|
@@ -45,6 +68,8 @@ export const TOOL_MODE_ORDER = [
|
|
|
45
68
|
'qwen',
|
|
46
69
|
'openhands',
|
|
47
70
|
'amp',
|
|
71
|
+
'rovo',
|
|
72
|
+
'gemini',
|
|
48
73
|
]
|
|
49
74
|
|
|
50
75
|
export function getToolMeta(mode) {
|
|
@@ -54,3 +79,61 @@ export function getToolMeta(mode) {
|
|
|
54
79
|
export function getToolModeOrder() {
|
|
55
80
|
return [...TOOL_MODE_ORDER]
|
|
56
81
|
}
|
|
82
|
+
|
|
83
|
+
// 📖 Regular tools: all tools EXCEPT rovo, gemini (which are CLI-only exclusives).
|
|
84
|
+
// 📖 Used as the default compatible set for normal provider models.
|
|
85
|
+
const REGULAR_TOOLS = Object.keys(TOOL_METADATA).filter(k => !TOOL_METADATA[k].cliOnly)
|
|
86
|
+
|
|
87
|
+
// 📖 Zen-only tools: OpenCode Zen models can ONLY run on OpenCode CLI / OpenCode Desktop.
|
|
88
|
+
const ZEN_COMPATIBLE_TOOLS = ['opencode', 'opencode-desktop']
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 📖 Returns the list of tool keys a model is compatible with.
|
|
92
|
+
* - Rovo models → only 'rovo'
|
|
93
|
+
* - Gemini models → only 'gemini'
|
|
94
|
+
* - OpenCode Zen models → only 'opencode', 'opencode-desktop'
|
|
95
|
+
* - Regular models → all non-cliOnly tools
|
|
96
|
+
* @param {string} providerKey — the source key from sources.js (e.g. 'nvidia', 'rovo', 'opencode-zen')
|
|
97
|
+
* @returns {string[]} — array of compatible tool keys
|
|
98
|
+
*/
|
|
99
|
+
export function getCompatibleTools(providerKey) {
|
|
100
|
+
if (providerKey === 'rovo') return ['rovo']
|
|
101
|
+
if (providerKey === 'gemini') return ['gemini']
|
|
102
|
+
if (providerKey === 'opencode-zen') return ZEN_COMPATIBLE_TOOLS
|
|
103
|
+
return REGULAR_TOOLS
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 📖 Checks whether a model from the given provider can run on the specified tool mode.
|
|
108
|
+
* @param {string} providerKey — source key
|
|
109
|
+
* @param {string} toolMode — active tool mode
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
export function isModelCompatibleWithTool(providerKey, toolMode) {
|
|
113
|
+
return getCompatibleTools(providerKey).includes(toolMode)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 📖 Finds compatible models with a similar SWE score to the selected one.
|
|
118
|
+
* 📖 Used by the incompatibility fallback overlay to suggest alternatives.
|
|
119
|
+
* @param {string} selectedSwe — SWE score string like '72.0%' or '-'
|
|
120
|
+
* @param {string} toolMode — current active tool mode
|
|
121
|
+
* @param {Array} allResults — the state.results array (each has .providerKey, .modelId, .label, .tier, .sweScore)
|
|
122
|
+
* @param {number} [maxResults=3] — max suggestions to return
|
|
123
|
+
* @returns {{ modelId: string, label: string, tier: string, sweScore: string, providerKey: string, sweDelta: number }[]}
|
|
124
|
+
*/
|
|
125
|
+
export function findSimilarCompatibleModels(selectedSwe, toolMode, allResults, maxResults = 3) {
|
|
126
|
+
const targetSwe = parseFloat(selectedSwe) || 0
|
|
127
|
+
return allResults
|
|
128
|
+
.filter(r => !r.hidden && isModelCompatibleWithTool(r.providerKey, toolMode))
|
|
129
|
+
.map(r => ({
|
|
130
|
+
modelId: r.modelId,
|
|
131
|
+
label: r.label,
|
|
132
|
+
tier: r.tier,
|
|
133
|
+
sweScore: r.sweScore || '-',
|
|
134
|
+
providerKey: r.providerKey,
|
|
135
|
+
sweDelta: Math.abs((parseFloat(r.sweScore) || 0) - targetSwe),
|
|
136
|
+
}))
|
|
137
|
+
.sort((a, b) => a.sweDelta - b.sweDelta)
|
|
138
|
+
.slice(0, maxResults)
|
|
139
|
+
}
|
package/src/utils.js
CHANGED
|
@@ -454,6 +454,8 @@ export function parseArgs(argv) {
|
|
|
454
454
|
const openHandsMode = flags.includes('--openhands')
|
|
455
455
|
const ampMode = flags.includes('--amp')
|
|
456
456
|
const piMode = flags.includes('--pi')
|
|
457
|
+
const rovoMode = flags.includes('--rovo')
|
|
458
|
+
const geminiMode = flags.includes('--gemini')
|
|
457
459
|
const noTelemetry = flags.includes('--no-telemetry')
|
|
458
460
|
const jsonMode = flags.includes('--json')
|
|
459
461
|
const helpMode = flags.includes('--help') || flags.includes('-h')
|
|
@@ -490,6 +492,8 @@ export function parseArgs(argv) {
|
|
|
490
492
|
openHandsMode,
|
|
491
493
|
ampMode,
|
|
492
494
|
piMode,
|
|
495
|
+
rovoMode,
|
|
496
|
+
geminiMode,
|
|
493
497
|
noTelemetry,
|
|
494
498
|
jsonMode,
|
|
495
499
|
helpMode,
|