free-coding-models 0.3.24 → 0.3.26
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 +51 -0
- package/README.md +20 -2
- package/package.json +1 -1
- package/sources.js +37 -19
- package/src/app.js +84 -2
- package/src/command-palette.js +1 -0
- package/src/constants.js +5 -2
- package/src/installed-models-manager.js +636 -0
- package/src/key-handler.js +548 -0
- package/src/mouse.js +186 -0
- package/src/overlays.js +170 -1
- package/src/render-table.js +169 -42
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')
|
|
@@ -451,6 +479,114 @@ export function createOverlayRenderers(state, deps) {
|
|
|
451
479
|
return cleared.join('\n')
|
|
452
480
|
}
|
|
453
481
|
|
|
482
|
+
// ─── Installed Models Manager overlay renderer ─────────────────────────────
|
|
483
|
+
// 📖 renderInstalledModels displays all models configured in external tools
|
|
484
|
+
// 📖 Shows tool configs, model lists, and provides actions (Launch, Disable, Reinstall)
|
|
485
|
+
function renderInstalledModels() {
|
|
486
|
+
const EL = '\x1b[K'
|
|
487
|
+
const lines = []
|
|
488
|
+
const cursorLineByRow = {}
|
|
489
|
+
|
|
490
|
+
lines.push('')
|
|
491
|
+
lines.push(` ${themeColors.accent('🚀')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
492
|
+
lines.push(` ${themeColors.textBold('🗂️ Installed Models Manager')}`)
|
|
493
|
+
lines.push('')
|
|
494
|
+
lines.push(themeColors.dim(' — models configured in your tools'))
|
|
495
|
+
|
|
496
|
+
if (state.installedModelsErrorMsg) {
|
|
497
|
+
lines.push(` ${themeColors.warning(state.installedModelsErrorMsg)}`)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (state.installedModelsErrorMsg === 'Scanning...') {
|
|
501
|
+
lines.push(themeColors.dim(' Scanning tool configs, please wait...'))
|
|
502
|
+
const targetLine = 5
|
|
503
|
+
state.installedModelsScrollOffset = keepOverlayTargetVisible(
|
|
504
|
+
state.installedModelsScrollOffset,
|
|
505
|
+
targetLine,
|
|
506
|
+
lines.length,
|
|
507
|
+
state.terminalRows
|
|
508
|
+
)
|
|
509
|
+
const { visible, offset } = sliceOverlayLines(lines, state.installedModelsScrollOffset, state.terminalRows)
|
|
510
|
+
state.installedModelsScrollOffset = offset
|
|
511
|
+
|
|
512
|
+
overlayLayout.installedModelsCursorToLine = cursorLineByRow
|
|
513
|
+
overlayLayout.installedModelsScrollOffset = offset
|
|
514
|
+
|
|
515
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
516
|
+
const cleared = tintedLines.map((l) => l + EL)
|
|
517
|
+
return cleared.join('\n')
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
lines.push('')
|
|
521
|
+
|
|
522
|
+
const scanResults = state.installedModelsData || []
|
|
523
|
+
|
|
524
|
+
if (scanResults.length === 0) {
|
|
525
|
+
lines.push(themeColors.dim(' No tool configs found.'))
|
|
526
|
+
lines.push(themeColors.dim(' Install a tool (Goose, Crush, Aider, etc.) to get started.'))
|
|
527
|
+
} else {
|
|
528
|
+
let globalIdx = 0
|
|
529
|
+
|
|
530
|
+
for (const toolResult of scanResults) {
|
|
531
|
+
const { toolMode, toolLabel, toolEmoji, configPath, isValid, hasManagedMarker, models } = toolResult
|
|
532
|
+
|
|
533
|
+
lines.push('')
|
|
534
|
+
const isCursor = globalIdx === state.installedModelsCursor
|
|
535
|
+
|
|
536
|
+
const statusIcon = isValid ? themeColors.successBold('✅') : themeColors.errorBold('⚠️')
|
|
537
|
+
const toolHeader = `${bullet(isCursor)}${toolEmoji} ${themeColors.textBold(toolLabel)} ${statusIcon}`
|
|
538
|
+
cursorLineByRow[globalIdx++] = lines.length
|
|
539
|
+
lines.push(isCursor ? themeColors.bgCursor(toolHeader) : toolHeader)
|
|
540
|
+
|
|
541
|
+
const configShortPath = configPath.replace(process.env.HOME || homedir(), '~')
|
|
542
|
+
lines.push(` ${themeColors.dim(configShortPath)}`)
|
|
543
|
+
|
|
544
|
+
if (!isValid) {
|
|
545
|
+
lines.push(themeColors.dim(' ⚠️ Config invalid or missing'))
|
|
546
|
+
} else if (models.length === 0) {
|
|
547
|
+
lines.push(themeColors.dim(' No models configured'))
|
|
548
|
+
} else {
|
|
549
|
+
const managedBadge = hasManagedMarker ? themeColors.info('• Managed by FCM') : themeColors.dim('• External config')
|
|
550
|
+
lines.push(` ${themeColors.success(`${models.length} model${models.length > 1 ? 's' : ''} configured`)} ${managedBadge}`)
|
|
551
|
+
|
|
552
|
+
for (const model of models) {
|
|
553
|
+
const isModelCursor = globalIdx === state.installedModelsCursor
|
|
554
|
+
const tierBadge = model.tier !== '-' ? themeColors.info(model.tier.padEnd(2)) : themeColors.dim(' ')
|
|
555
|
+
const externalBadge = model.isExternal ? themeColors.dim('[external]') : ''
|
|
556
|
+
|
|
557
|
+
const modelRow = ` • ${model.label} ${tierBadge} ${externalBadge}`
|
|
558
|
+
cursorLineByRow[globalIdx++] = lines.length
|
|
559
|
+
lines.push(isModelCursor ? themeColors.bgCursor(modelRow) : modelRow)
|
|
560
|
+
|
|
561
|
+
if (isModelCursor) {
|
|
562
|
+
lines.push(` ${themeColors.dim('[Enter] Launch [D] Disable')}`)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
lines.push('')
|
|
570
|
+
lines.push(themeColors.dim(' ↑↓ Navigate Enter=Launch D=Disable Esc=Close'))
|
|
571
|
+
|
|
572
|
+
const targetLine = cursorLineByRow[state.installedModelsCursor] ?? 0
|
|
573
|
+
state.installedModelsScrollOffset = keepOverlayTargetVisible(
|
|
574
|
+
state.installedModelsScrollOffset,
|
|
575
|
+
targetLine,
|
|
576
|
+
lines.length,
|
|
577
|
+
state.terminalRows
|
|
578
|
+
)
|
|
579
|
+
const { visible, offset } = sliceOverlayLines(lines, state.installedModelsScrollOffset, state.terminalRows)
|
|
580
|
+
state.installedModelsScrollOffset = offset
|
|
581
|
+
|
|
582
|
+
overlayLayout.installedModelsCursorToLine = cursorLineByRow
|
|
583
|
+
overlayLayout.installedModelsScrollOffset = offset
|
|
584
|
+
|
|
585
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
586
|
+
const cleared = tintedLines.map((l) => l + EL)
|
|
587
|
+
return cleared.join('\n')
|
|
588
|
+
}
|
|
589
|
+
|
|
454
590
|
// ─── Missing-tool install confirmation overlay ────────────────────────────
|
|
455
591
|
// 📖 renderToolInstallPrompt keeps the user inside the TUI long enough to
|
|
456
592
|
// 📖 confirm the auto-install, then the key handler exits the alt screen and
|
|
@@ -684,6 +820,18 @@ export function createOverlayRenderers(state, deps) {
|
|
|
684
820
|
const top = Math.max(1, Math.floor((terminalRows - panelHeight) / 2) + 1)
|
|
685
821
|
const left = Math.max(1, Math.floor((terminalCols - panelOuterWidth) / 2) + 1)
|
|
686
822
|
|
|
823
|
+
// 📖 Mouse support: record CP layout so clicks inside the modal can select items.
|
|
824
|
+
// 📖 Body rows start after 2 blank-padding lines + headerLines (3).
|
|
825
|
+
const bodyStartRow = top + 2 + headerLines.length // 📖 1-based terminal row of first body line
|
|
826
|
+
overlayLayout.commandPaletteCursorToLine = { ...cursorLineByRow }
|
|
827
|
+
overlayLayout.commandPaletteScrollOffset = state.commandPaletteScrollOffset
|
|
828
|
+
overlayLayout.commandPaletteBodyStartRow = bodyStartRow
|
|
829
|
+
overlayLayout.commandPaletteBodyRows = bodyRows
|
|
830
|
+
overlayLayout.commandPaletteLeft = left
|
|
831
|
+
overlayLayout.commandPaletteRight = left + panelOuterWidth - 1
|
|
832
|
+
overlayLayout.commandPaletteTop = top
|
|
833
|
+
overlayLayout.commandPaletteBottom = top + panelHeight - 1
|
|
834
|
+
|
|
687
835
|
const tintedLines = paddedPanelLines.map((line) => {
|
|
688
836
|
const padded = padEndDisplay(line, panelOuterWidth)
|
|
689
837
|
return themeColors.overlayBgCommandPalette(padded)
|
|
@@ -861,6 +1009,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
861
1009
|
const opt = q.options[i]
|
|
862
1010
|
const isCursor = i === state.recommendCursor
|
|
863
1011
|
const label = isCursor ? themeColors.textBold(opt.label) : themeColors.text(opt.label)
|
|
1012
|
+
// 📖 Mouse support: record the 1-based terminal row of each option
|
|
1013
|
+
// 📖 lines.length is the 0-based index → +1 = 1-based row
|
|
1014
|
+
overlayLayout.recommendOptionRows = overlayLayout.recommendOptionRows || {}
|
|
1015
|
+
overlayLayout.recommendOptionRows[i] = lines.length + 1
|
|
864
1016
|
lines.push(`${bullet(isCursor)}${label}`)
|
|
865
1017
|
}
|
|
866
1018
|
|
|
@@ -1206,6 +1358,21 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1206
1358
|
// 📖 Use scrolling with overlay handler
|
|
1207
1359
|
const { visible, offset } = sliceOverlayLines(lines, state.changelogScrollOffset, state.terminalRows)
|
|
1208
1360
|
state.changelogScrollOffset = offset
|
|
1361
|
+
|
|
1362
|
+
// 📖 Mouse support: record changelog layout for click-to-select versions
|
|
1363
|
+
overlayLayout.changelogScrollOffset = offset
|
|
1364
|
+
// 📖 In index phase, version items start at line 4 (header + blank + title + instructions)
|
|
1365
|
+
// 📖 Each version occupies exactly one line. changelogCursorToLine maps cursor → line index.
|
|
1366
|
+
if (state.changelogPhase === 'index') {
|
|
1367
|
+
const map = {}
|
|
1368
|
+
for (let i = 0; i < versionList.length; i++) {
|
|
1369
|
+
map[i] = 4 + i // 📖 3 header-ish lines + 1 blank before version list
|
|
1370
|
+
}
|
|
1371
|
+
overlayLayout.changelogCursorToLine = map
|
|
1372
|
+
} else {
|
|
1373
|
+
overlayLayout.changelogCursorToLine = {}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1209
1376
|
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgChangelog, state.terminalCols)
|
|
1210
1377
|
const cleared = tintedLines.map(l => l + EL)
|
|
1211
1378
|
return cleared.join('\n')
|
|
@@ -1322,8 +1489,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1322
1489
|
renderRecommend,
|
|
1323
1490
|
renderFeedback,
|
|
1324
1491
|
renderChangelog,
|
|
1492
|
+
renderInstalledModels,
|
|
1325
1493
|
renderIncompatibleFallback,
|
|
1326
1494
|
startRecommendAnalysis,
|
|
1327
1495
|
stopRecommendAnalysis,
|
|
1496
|
+
overlayLayout, // 📖 Mouse support: exposes cursor-to-line maps for click handling
|
|
1328
1497
|
}
|
|
1329
1498
|
}
|