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/mouse.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file mouse.js
|
|
3
|
+
* @description Terminal mouse tracking infrastructure for the TUI.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* Provides SGR (mode 1006) mouse event parsing and tracking enable/disable sequences.
|
|
7
|
+
* SGR mode is preferred over X10/normal because it supports coordinates > 223
|
|
8
|
+
* and distinguishes press vs release events cleanly.
|
|
9
|
+
*
|
|
10
|
+
* Mouse events arrive as raw escape sequences on stdin when tracking is enabled:
|
|
11
|
+
* Press: \x1b[<Btn;X;YM
|
|
12
|
+
* Release: \x1b[<Btn;X;Ym
|
|
13
|
+
*
|
|
14
|
+
* Button encoding (Btn field):
|
|
15
|
+
* 0 = left click, 1 = middle click, 2 = right click
|
|
16
|
+
* 32 = left drag, 33 = middle drag, 34 = right drag
|
|
17
|
+
* 64 = scroll up, 65 = scroll down
|
|
18
|
+
* +4 = Shift held, +8 = Meta/Alt held, +16 = Control held
|
|
19
|
+
*
|
|
20
|
+
* Coordinates are 1-based in SGR mode (col 1, row 1 = top-left).
|
|
21
|
+
*
|
|
22
|
+
* ⚙️ Key configuration:
|
|
23
|
+
* - MOUSE_ENABLE: appended to ALT_ENTER to start mouse tracking on TUI init
|
|
24
|
+
* - MOUSE_DISABLE: prepended to ALT_LEAVE to stop mouse tracking on TUI exit
|
|
25
|
+
* - DOUBLE_CLICK_MS: maximum gap between two clicks to count as double-click
|
|
26
|
+
*
|
|
27
|
+
* @functions
|
|
28
|
+
* → parseMouseEvent(data) — Parse raw stdin buffer into structured mouse event
|
|
29
|
+
* → createMouseHandler(opts) — Create a stdin 'data' listener that emits mouse events
|
|
30
|
+
*
|
|
31
|
+
* @exports
|
|
32
|
+
* MOUSE_ENABLE, MOUSE_DISABLE,
|
|
33
|
+
* parseMouseEvent, createMouseHandler
|
|
34
|
+
*
|
|
35
|
+
* @see src/app.js — wires the mouse data listener alongside keypress
|
|
36
|
+
* @see src/key-handler.js — receives parsed mouse events for UI actions
|
|
37
|
+
* @see src/constants.js — ALT_ENTER / ALT_LEAVE include mouse sequences
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// 📖 SGR mouse mode (1006) sends coordinates as decimal numbers terminated by M/m,
|
|
41
|
+
// 📖 supporting terminals wider than 223 columns (unlike X10 mode).
|
|
42
|
+
// 📖 Mode 1000 = basic button tracking (press + release).
|
|
43
|
+
// 📖 Mode 1002 = button-event tracking (adds drag reporting).
|
|
44
|
+
// 📖 Mode 1003 = any-event tracking (adds mouse movement) — intentionally NOT used
|
|
45
|
+
// 📖 because movement floods stdin and we don't need hover.
|
|
46
|
+
export const MOUSE_ENABLE = '\x1b[?1000h\x1b[?1002h\x1b[?1006h'
|
|
47
|
+
export const MOUSE_DISABLE = '\x1b[?1006l\x1b[?1002l\x1b[?1000l'
|
|
48
|
+
|
|
49
|
+
// 📖 Double-click detection window in milliseconds.
|
|
50
|
+
const DOUBLE_CLICK_MS = 400
|
|
51
|
+
|
|
52
|
+
// 📖 Regex to match SGR mouse sequences: \x1b[<Btn;Col;Row[Mm]
|
|
53
|
+
// 📖 Groups: 1=button, 2=column(x), 3=row(y), 4=M(press)|m(release)
|
|
54
|
+
const SGR_MOUSE_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 📖 parseMouseEvents: Extract all SGR mouse events from a raw stdin data chunk.
|
|
58
|
+
* 📖 A single data chunk can contain multiple mouse events (e.g. rapid scrolling).
|
|
59
|
+
* @param {string|Buffer} data — raw stdin data
|
|
60
|
+
* @returns {Array<{button: number, x: number, y: number, type: string, shift: boolean, meta: boolean, ctrl: boolean}>}
|
|
61
|
+
*/
|
|
62
|
+
export function parseMouseEvents(data) {
|
|
63
|
+
const str = typeof data === 'string' ? data : data.toString('utf8')
|
|
64
|
+
const events = []
|
|
65
|
+
let match
|
|
66
|
+
|
|
67
|
+
// 📖 Reset regex lastIndex for reuse
|
|
68
|
+
SGR_MOUSE_RE.lastIndex = 0
|
|
69
|
+
|
|
70
|
+
while ((match = SGR_MOUSE_RE.exec(str)) !== null) {
|
|
71
|
+
const rawBtn = parseInt(match[1], 10)
|
|
72
|
+
const x = parseInt(match[2], 10) // 📖 1-based column
|
|
73
|
+
const y = parseInt(match[3], 10) // 📖 1-based row
|
|
74
|
+
const isRelease = match[4] === 'm'
|
|
75
|
+
|
|
76
|
+
// 📖 Extract modifier keys from the button field
|
|
77
|
+
const shift = !!(rawBtn & 4)
|
|
78
|
+
const meta = !!(rawBtn & 8)
|
|
79
|
+
const ctrl = !!(rawBtn & 16)
|
|
80
|
+
|
|
81
|
+
// 📖 Strip modifier bits to get the base button
|
|
82
|
+
const baseBtn = rawBtn & ~(4 | 8 | 16)
|
|
83
|
+
|
|
84
|
+
let type, button
|
|
85
|
+
|
|
86
|
+
if (baseBtn === 64) {
|
|
87
|
+
type = 'scroll-up'
|
|
88
|
+
button = 'scroll-up'
|
|
89
|
+
} else if (baseBtn === 65) {
|
|
90
|
+
type = 'scroll-down'
|
|
91
|
+
button = 'scroll-down'
|
|
92
|
+
} else if (baseBtn >= 32 && baseBtn <= 34) {
|
|
93
|
+
type = 'drag'
|
|
94
|
+
button = baseBtn === 32 ? 'left' : baseBtn === 33 ? 'middle' : 'right'
|
|
95
|
+
} else if (isRelease) {
|
|
96
|
+
type = 'release'
|
|
97
|
+
button = baseBtn === 0 ? 'left' : baseBtn === 1 ? 'middle' : 'right'
|
|
98
|
+
} else {
|
|
99
|
+
type = 'press'
|
|
100
|
+
button = baseBtn === 0 ? 'left' : baseBtn === 1 ? 'middle' : 'right'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
events.push({ type, button, x, y, shift, meta, ctrl })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return events
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 📖 containsMouseSequence: Quick check if a data chunk contains any SGR mouse sequence.
|
|
111
|
+
* 📖 Used to prevent the keypress handler from processing mouse data as keypresses.
|
|
112
|
+
* @param {string|Buffer} data
|
|
113
|
+
* @returns {boolean}
|
|
114
|
+
*/
|
|
115
|
+
export function containsMouseSequence(data) {
|
|
116
|
+
const str = typeof data === 'string' ? data : data.toString('utf8')
|
|
117
|
+
return str.includes('\x1b[<')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 📖 createMouseHandler: Factory that returns a stdin 'data' callback for mouse events.
|
|
122
|
+
* 📖 Handles double-click detection internally by tracking the last click position/time.
|
|
123
|
+
*
|
|
124
|
+
* @param {object} opts
|
|
125
|
+
* @param {function} opts.onMouseEvent — callback receiving structured mouse events:
|
|
126
|
+
* { type: 'click'|'double-click'|'scroll-up'|'scroll-down'|'drag', button, x, y, shift, meta, ctrl }
|
|
127
|
+
* @returns {function} — attach to process.stdin.on('data', returnedFn)
|
|
128
|
+
*/
|
|
129
|
+
export function createMouseHandler({ onMouseEvent }) {
|
|
130
|
+
// 📖 Double-click tracking state
|
|
131
|
+
let lastClickX = -1
|
|
132
|
+
let lastClickY = -1
|
|
133
|
+
let lastClickTime = 0
|
|
134
|
+
|
|
135
|
+
return (data) => {
|
|
136
|
+
const str = typeof data === 'string' ? data : data.toString('utf8')
|
|
137
|
+
|
|
138
|
+
// 📖 Only process data that contains mouse sequences
|
|
139
|
+
if (!str.includes('\x1b[<')) return
|
|
140
|
+
|
|
141
|
+
const events = parseMouseEvents(str)
|
|
142
|
+
|
|
143
|
+
for (const evt of events) {
|
|
144
|
+
// 📖 Scroll events are emitted immediately (no press/release distinction)
|
|
145
|
+
if (evt.type === 'scroll-up' || evt.type === 'scroll-down') {
|
|
146
|
+
onMouseEvent(evt)
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 📖 Drag events forwarded as-is
|
|
151
|
+
if (evt.type === 'drag') {
|
|
152
|
+
onMouseEvent(evt)
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 📖 Only emit click on release (not press) to match expected click semantics.
|
|
157
|
+
// 📖 This prevents double-firing and feels more natural to the user.
|
|
158
|
+
if (evt.type === 'release' && evt.button === 'left') {
|
|
159
|
+
const now = Date.now()
|
|
160
|
+
const isDoubleClick =
|
|
161
|
+
(now - lastClickTime) < DOUBLE_CLICK_MS &&
|
|
162
|
+
evt.x === lastClickX &&
|
|
163
|
+
evt.y === lastClickY
|
|
164
|
+
|
|
165
|
+
if (isDoubleClick) {
|
|
166
|
+
onMouseEvent({ ...evt, type: 'double-click' })
|
|
167
|
+
// 📖 Reset so a third click doesn't count as another double-click
|
|
168
|
+
lastClickTime = 0
|
|
169
|
+
lastClickX = -1
|
|
170
|
+
lastClickY = -1
|
|
171
|
+
} else {
|
|
172
|
+
onMouseEvent({ ...evt, type: 'click' })
|
|
173
|
+
lastClickTime = now
|
|
174
|
+
lastClickX = evt.x
|
|
175
|
+
lastClickY = evt.y
|
|
176
|
+
}
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 📖 Right-click and middle-click: emit on release
|
|
181
|
+
if (evt.type === 'release') {
|
|
182
|
+
onMouseEvent({ ...evt, type: 'click' })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
package/src/overlays.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* 📖 Feedback overlay (I key) combines feature requests + bug reports in one left-aligned input
|
|
15
15
|
*
|
|
16
16
|
* → Functions:
|
|
17
|
-
* - `createOverlayRenderers` — returns renderer + analysis helpers
|
|
17
|
+
* - `createOverlayRenderers` — returns renderer + analysis helpers + overlayLayout
|
|
18
18
|
*
|
|
19
19
|
* @exports { createOverlayRenderers }
|
|
20
20
|
* @see ./key-handler.js — handles keypresses for all overlay interactions
|
|
@@ -85,6 +85,29 @@ export function createOverlayRenderers(state, deps) {
|
|
|
85
85
|
return lines
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// 📖 Overlay layout tracking: records cursor-to-line mappings and scroll offsets
|
|
89
|
+
// 📖 so the mouse handler can map terminal click coordinates → overlay cursor positions.
|
|
90
|
+
// 📖 Updated each render frame by the active overlay renderer.
|
|
91
|
+
const overlayLayout = {
|
|
92
|
+
settingsCursorToLine: {}, // 📖 cursor index → line index in pre-scroll lines array
|
|
93
|
+
settingsScrollOffset: 0, // 📖 current scroll offset applied by sliceOverlayLines
|
|
94
|
+
settingsMaxRow: 0, // 📖 maximum valid settingsCursor index
|
|
95
|
+
installEndpointsCursorToLine: {},
|
|
96
|
+
installEndpointsScrollOffset: 0,
|
|
97
|
+
installEndpointsMaxRow: 0,
|
|
98
|
+
commandPaletteCursorToLine: {},
|
|
99
|
+
commandPaletteScrollOffset: 0,
|
|
100
|
+
commandPaletteBodyStartRow: 0, // 📖 1-based terminal row where CP results begin
|
|
101
|
+
commandPaletteBodyRows: 0,
|
|
102
|
+
commandPaletteLeft: 0,
|
|
103
|
+
commandPaletteRight: 0,
|
|
104
|
+
commandPaletteTop: 0,
|
|
105
|
+
commandPaletteBottom: 0,
|
|
106
|
+
changelogCursorToLine: {},
|
|
107
|
+
changelogScrollOffset: 0,
|
|
108
|
+
recommendOptionRows: {}, // 📖 option index → 1-based terminal row (questionnaire phase)
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
// ─── Settings screen renderer ─────────────────────────────────────────────
|
|
89
112
|
// 📖 renderSettings: Draw the settings overlay in the alt screen buffer.
|
|
90
113
|
// 📖 Shows all providers with their API key (masked) + enabled state.
|
|
@@ -287,6 +310,11 @@ export function createOverlayRenderers(state, deps) {
|
|
|
287
310
|
const { visible, offset } = sliceOverlayLines(lines, state.settingsScrollOffset, state.terminalRows)
|
|
288
311
|
state.settingsScrollOffset = offset
|
|
289
312
|
|
|
313
|
+
// 📖 Mouse support: record layout so click handler can map Y → settingsCursor
|
|
314
|
+
overlayLayout.settingsCursorToLine = { ...cursorLineByRow }
|
|
315
|
+
overlayLayout.settingsScrollOffset = offset
|
|
316
|
+
overlayLayout.settingsMaxRow = changelogViewRowIdx
|
|
317
|
+
|
|
290
318
|
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
291
319
|
const cleared = tintedLines.map(l => l + EL)
|
|
292
320
|
return cleared.join('\n')
|
|
@@ -684,6 +712,18 @@ export function createOverlayRenderers(state, deps) {
|
|
|
684
712
|
const top = Math.max(1, Math.floor((terminalRows - panelHeight) / 2) + 1)
|
|
685
713
|
const left = Math.max(1, Math.floor((terminalCols - panelOuterWidth) / 2) + 1)
|
|
686
714
|
|
|
715
|
+
// 📖 Mouse support: record CP layout so clicks inside the modal can select items.
|
|
716
|
+
// 📖 Body rows start after 2 blank-padding lines + headerLines (3).
|
|
717
|
+
const bodyStartRow = top + 2 + headerLines.length // 📖 1-based terminal row of first body line
|
|
718
|
+
overlayLayout.commandPaletteCursorToLine = { ...cursorLineByRow }
|
|
719
|
+
overlayLayout.commandPaletteScrollOffset = state.commandPaletteScrollOffset
|
|
720
|
+
overlayLayout.commandPaletteBodyStartRow = bodyStartRow
|
|
721
|
+
overlayLayout.commandPaletteBodyRows = bodyRows
|
|
722
|
+
overlayLayout.commandPaletteLeft = left
|
|
723
|
+
overlayLayout.commandPaletteRight = left + panelOuterWidth - 1
|
|
724
|
+
overlayLayout.commandPaletteTop = top
|
|
725
|
+
overlayLayout.commandPaletteBottom = top + panelHeight - 1
|
|
726
|
+
|
|
687
727
|
const tintedLines = paddedPanelLines.map((line) => {
|
|
688
728
|
const padded = padEndDisplay(line, panelOuterWidth)
|
|
689
729
|
return themeColors.overlayBgCommandPalette(padded)
|
|
@@ -764,7 +804,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
764
804
|
lines.push(` ${key('W')} Toggle ping mode ${hint('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
|
|
765
805
|
lines.push(` ${key('Ctrl+P')} Open ⚡️ command palette ${hint('(search and run actions quickly)')}`)
|
|
766
806
|
lines.push(` ${key('E')} Toggle configured models only ${hint('(enabled by default)')}`)
|
|
767
|
-
lines.push(` ${key('Z')} Cycle tool mode ${hint('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Qwen → OpenHands → Amp)')}`)
|
|
807
|
+
lines.push(` ${key('Z')} Cycle tool mode ${hint('(📦 OpenCode → 📦 Desktop → 🦞 OpenClaw → 💘 Crush → 🪿 Goose → π Pi → 🛠 Aider → 🐉 Qwen → 🤲 OpenHands → ⚡ Amp → 🦘 Rovo → ♊ Gemini)')}`)
|
|
768
808
|
lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(⭐ persisted across sessions)')}`)
|
|
769
809
|
lines.push(` ${key('Y')} Toggle favorites mode ${hint('(Pinned + always visible ↔ Normal filter/sort behavior)')}`)
|
|
770
810
|
lines.push(` ${key('X')} Clear active text filter ${hint('(remove custom query applied from ⚡️ Command Palette)')}`)
|
|
@@ -861,6 +901,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
861
901
|
const opt = q.options[i]
|
|
862
902
|
const isCursor = i === state.recommendCursor
|
|
863
903
|
const label = isCursor ? themeColors.textBold(opt.label) : themeColors.text(opt.label)
|
|
904
|
+
// 📖 Mouse support: record the 1-based terminal row of each option
|
|
905
|
+
// 📖 lines.length is the 0-based index → +1 = 1-based row
|
|
906
|
+
overlayLayout.recommendOptionRows = overlayLayout.recommendOptionRows || {}
|
|
907
|
+
overlayLayout.recommendOptionRows[i] = lines.length + 1
|
|
864
908
|
lines.push(`${bullet(isCursor)}${label}`)
|
|
865
909
|
}
|
|
866
910
|
|
|
@@ -1206,6 +1250,21 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1206
1250
|
// 📖 Use scrolling with overlay handler
|
|
1207
1251
|
const { visible, offset } = sliceOverlayLines(lines, state.changelogScrollOffset, state.terminalRows)
|
|
1208
1252
|
state.changelogScrollOffset = offset
|
|
1253
|
+
|
|
1254
|
+
// 📖 Mouse support: record changelog layout for click-to-select versions
|
|
1255
|
+
overlayLayout.changelogScrollOffset = offset
|
|
1256
|
+
// 📖 In index phase, version items start at line 4 (header + blank + title + instructions)
|
|
1257
|
+
// 📖 Each version occupies exactly one line. changelogCursorToLine maps cursor → line index.
|
|
1258
|
+
if (state.changelogPhase === 'index') {
|
|
1259
|
+
const map = {}
|
|
1260
|
+
for (let i = 0; i < versionList.length; i++) {
|
|
1261
|
+
map[i] = 4 + i // 📖 3 header-ish lines + 1 blank before version list
|
|
1262
|
+
}
|
|
1263
|
+
overlayLayout.changelogCursorToLine = map
|
|
1264
|
+
} else {
|
|
1265
|
+
overlayLayout.changelogCursorToLine = {}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1209
1268
|
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgChangelog, state.terminalCols)
|
|
1210
1269
|
const cleared = tintedLines.map(l => l + EL)
|
|
1211
1270
|
return cleared.join('\n')
|
|
@@ -1217,6 +1276,102 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1217
1276
|
if (state.recommendPingTimer) { clearInterval(state.recommendPingTimer); state.recommendPingTimer = null }
|
|
1218
1277
|
}
|
|
1219
1278
|
|
|
1279
|
+
// ─── Incompatible fallback overlay ─────────────────────────────────────────
|
|
1280
|
+
// 📖 renderIncompatibleFallback shows when user presses Enter on a model that
|
|
1281
|
+
// 📖 is NOT compatible with the active tool. Two sections:
|
|
1282
|
+
// 📖 Section 1: "Switch to a compatible tool" — lists tools the model CAN run on
|
|
1283
|
+
// 📖 Section 2: "Use a similar model" — lists SWE-similar models compatible with current tool
|
|
1284
|
+
// 📖 Cursor navigates a flat list across both sections. Enter executes, Esc cancels.
|
|
1285
|
+
function renderIncompatibleFallback() {
|
|
1286
|
+
const EL = '\x1b[K'
|
|
1287
|
+
const lines = []
|
|
1288
|
+
const cursorLineByRow = {}
|
|
1289
|
+
|
|
1290
|
+
const model = state.incompatibleFallbackModel
|
|
1291
|
+
const tools = state.incompatibleFallbackTools || []
|
|
1292
|
+
const similarModels = state.incompatibleFallbackSimilarModels || []
|
|
1293
|
+
const totalItems = tools.length + similarModels.length
|
|
1294
|
+
const activeMeta = getToolMeta(state.mode)
|
|
1295
|
+
|
|
1296
|
+
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')}`)
|
|
1297
|
+
lines.push(` ${chalk.bold('⚠️ Incompatible Model')}`)
|
|
1298
|
+
lines.push('')
|
|
1299
|
+
|
|
1300
|
+
if (!model) {
|
|
1301
|
+
lines.push(chalk.red(' No model data available.'))
|
|
1302
|
+
lines.push('')
|
|
1303
|
+
lines.push(chalk.dim(' Esc Close'))
|
|
1304
|
+
} else {
|
|
1305
|
+
// 📖 Header: explain why it's incompatible
|
|
1306
|
+
const tierFn = TIER_COLOR[model.tier] ?? ((text) => themeColors.text(text))
|
|
1307
|
+
lines.push(` ${themeColors.textBold(model.label)} ${tierFn(model.tier)}`)
|
|
1308
|
+
lines.push(chalk.dim(` This model cannot run on ${activeMeta.emoji} ${activeMeta.label}.`))
|
|
1309
|
+
lines.push('')
|
|
1310
|
+
|
|
1311
|
+
// 📖 Section 1: Switch to a compatible tool
|
|
1312
|
+
if (tools.length > 0) {
|
|
1313
|
+
lines.push(` ${themeColors.textBold('Switch to a compatible tool:')}`)
|
|
1314
|
+
lines.push('')
|
|
1315
|
+
|
|
1316
|
+
for (let i = 0; i < tools.length; i++) {
|
|
1317
|
+
const toolKey = tools[i]
|
|
1318
|
+
const meta = getToolMeta(toolKey)
|
|
1319
|
+
const [r, g, b] = meta.color || [200, 200, 200]
|
|
1320
|
+
const coloredLabel = chalk.rgb(r, g, b)(`${meta.emoji} ${meta.label}`)
|
|
1321
|
+
const isCursor = state.incompatibleFallbackCursor === i
|
|
1322
|
+
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1323
|
+
const row = `${bullet}${coloredLabel}`
|
|
1324
|
+
cursorLineByRow[i] = lines.length
|
|
1325
|
+
lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
|
|
1326
|
+
}
|
|
1327
|
+
lines.push('')
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// 📖 Section 2: Use a similar model
|
|
1331
|
+
if (similarModels.length > 0) {
|
|
1332
|
+
lines.push(` ${themeColors.textBold('Or pick a similar model for')} ${activeMeta.emoji} ${themeColors.textBold(activeMeta.label + ':')}`)
|
|
1333
|
+
lines.push('')
|
|
1334
|
+
|
|
1335
|
+
for (let i = 0; i < similarModels.length; i++) {
|
|
1336
|
+
const sm = similarModels[i]
|
|
1337
|
+
const flatIdx = tools.length + i
|
|
1338
|
+
const tierFnSm = TIER_COLOR[sm.tier] ?? ((text) => themeColors.text(text))
|
|
1339
|
+
const isCursor = state.incompatibleFallbackCursor === flatIdx
|
|
1340
|
+
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1341
|
+
const sweLabel = sm.sweScore !== '-' ? `SWE ${sm.sweScore}` : 'SWE —'
|
|
1342
|
+
const row = `${bullet}${themeColors.textBold(sm.label)} ${tierFnSm(sm.tier)} ${chalk.dim(sweLabel)}`
|
|
1343
|
+
cursorLineByRow[flatIdx] = lines.length
|
|
1344
|
+
lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
|
|
1345
|
+
}
|
|
1346
|
+
lines.push('')
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (totalItems === 0) {
|
|
1350
|
+
lines.push(chalk.yellow(' No compatible tools or similar models found.'))
|
|
1351
|
+
lines.push('')
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
lines.push(chalk.dim(' ↑↓ Navigate • Enter Confirm • Esc Cancel'))
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
lines.push('')
|
|
1358
|
+
|
|
1359
|
+
// 📖 Scroll management — same pattern as other overlays
|
|
1360
|
+
const targetLine = cursorLineByRow[state.incompatibleFallbackCursor] ?? 0
|
|
1361
|
+
state.incompatibleFallbackScrollOffset = keepOverlayTargetVisible(
|
|
1362
|
+
state.incompatibleFallbackScrollOffset,
|
|
1363
|
+
targetLine,
|
|
1364
|
+
lines.length,
|
|
1365
|
+
state.terminalRows
|
|
1366
|
+
)
|
|
1367
|
+
const { visible, offset } = sliceOverlayLines(lines, state.incompatibleFallbackScrollOffset, state.terminalRows)
|
|
1368
|
+
state.incompatibleFallbackScrollOffset = offset
|
|
1369
|
+
|
|
1370
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
1371
|
+
const cleared = tintedLines.map(l => l + EL)
|
|
1372
|
+
return cleared.join('\n')
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1220
1375
|
return {
|
|
1221
1376
|
renderSettings,
|
|
1222
1377
|
renderInstallEndpoints,
|
|
@@ -1226,7 +1381,9 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1226
1381
|
renderRecommend,
|
|
1227
1382
|
renderFeedback,
|
|
1228
1383
|
renderChangelog,
|
|
1384
|
+
renderIncompatibleFallback,
|
|
1229
1385
|
startRecommendAnalysis,
|
|
1230
1386
|
stopRecommendAnalysis,
|
|
1387
|
+
overlayLayout, // 📖 Mouse support: exposes cursor-to-line maps for click handling
|
|
1231
1388
|
}
|
|
1232
1389
|
}
|
package/src/provider-metadata.js
CHANGED
|
@@ -58,6 +58,7 @@ export const ENV_VAR_NAMES = {
|
|
|
58
58
|
cloudflare: 'CLOUDFLARE_API_TOKEN',
|
|
59
59
|
perplexity: 'PERPLEXITY_API_KEY',
|
|
60
60
|
zai: 'ZAI_API_KEY',
|
|
61
|
+
gemini: 'GEMINI_API_KEY',
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
// 📖 OPENCODE_MODEL_MAP: sparse table of model IDs that differ between sources.js and OpenCode's
|
|
@@ -225,4 +226,28 @@ export const PROVIDER_METADATA = {
|
|
|
225
226
|
signupHint: 'Install @mariozechner/pi-coding-agent and set ANTHROPIC_API_KEY',
|
|
226
227
|
rateLimits: 'Depends on provider subscription (e.g., Anthropic, OpenAI)',
|
|
227
228
|
},
|
|
229
|
+
rovo: {
|
|
230
|
+
label: 'Rovo Dev CLI',
|
|
231
|
+
color: chalk.rgb(148, 163, 184), // slate blue
|
|
232
|
+
signupUrl: 'https://www.atlassian.com/rovo',
|
|
233
|
+
signupHint: 'Install ACLI and run: acli rovodev auth login',
|
|
234
|
+
rateLimits: 'Free tier: 5M tokens/day (beta, requires Atlassian account)',
|
|
235
|
+
cliOnly: true,
|
|
236
|
+
},
|
|
237
|
+
gemini: {
|
|
238
|
+
label: 'Gemini CLI',
|
|
239
|
+
color: chalk.rgb(66, 165, 245), // blue
|
|
240
|
+
signupUrl: 'https://github.com/google-gemini/gemini-cli',
|
|
241
|
+
signupHint: 'Install: npm install -g @google/gemini-cli',
|
|
242
|
+
rateLimits: 'Free tier: 1,000 req/day (personal Google account, no credit card)',
|
|
243
|
+
cliOnly: true,
|
|
244
|
+
},
|
|
245
|
+
'opencode-zen': {
|
|
246
|
+
label: 'OpenCode Zen',
|
|
247
|
+
color: chalk.rgb(139, 92, 246), // violet — distinctive from other providers
|
|
248
|
+
signupUrl: 'https://opencode.ai/auth',
|
|
249
|
+
signupHint: 'Login at opencode.ai/auth to get your Zen API key',
|
|
250
|
+
rateLimits: 'Free tier models — requires OpenCode Zen API key',
|
|
251
|
+
zenOnly: true,
|
|
252
|
+
},
|
|
228
253
|
}
|
package/src/render-helpers.js
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
* - chalk: Terminal colors and formatting
|
|
37
37
|
* - ../src/constants.js: OVERLAY_PANEL_WIDTH, TABLE_FIXED_LINES
|
|
38
38
|
* - ../src/utils.js: sortResults
|
|
39
|
+
* - ../src/tool-metadata.js: isModelCompatibleWithTool (for compatible-first partition)
|
|
39
40
|
*
|
|
40
41
|
* ⚙️ Configuration:
|
|
41
42
|
* - OVERLAY_PANEL_WIDTH: Fixed width for overlay panels (from constants.js)
|
|
@@ -184,7 +185,6 @@ export function sortResultsWithPinnedFavorites(results, sortColumn, sortDirectio
|
|
|
184
185
|
)
|
|
185
186
|
return [...recommendedRows, ...nonRecommendedRows]
|
|
186
187
|
}
|
|
187
|
-
|
|
188
188
|
const recommendedRows = results
|
|
189
189
|
.filter((r) => r.isRecommended && !r.isFavorite)
|
|
190
190
|
.sort((a, b) => (b.recommendScore || 0) - (a.recommendScore || 0))
|