free-coding-models 0.5.4 → 0.5.6
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/README.md +8 -5
- package/bin/free-coding-models.js +29 -8
- package/changelog/v0.5.5.md +16 -0
- package/changelog/v0.5.6.md +24 -0
- package/package.json +4 -4
- package/src/core/changelog-loader.js +5 -1
- package/src/core/router-daemon.js +11 -0
- package/src/core/updater.js +174 -10
- package/src/tui/app.js +11 -31
- package/src/tui/render-table.js +9 -3
- package/src/tui/tui-state.js +6 -0
- package/web/dist/assets/index-Blp9QJev.js +39 -0
- package/web/dist/assets/{index-BrpHevg4.css → index-Cz_aCLTR.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/server.js +158 -2
- package/web/src/App.jsx +107 -58
- package/web/src/components/changelog/ChangelogView.jsx +135 -0
- package/web/src/components/changelog/ChangelogView.module.css +160 -0
- package/web/src/components/help/HelpView.jsx +188 -0
- package/web/src/components/help/HelpView.module.css +157 -0
- package/web/src/components/layout/Header.jsx +8 -3
- package/web/src/components/palette/CommandPalette.jsx +228 -74
- package/web/src/components/settings/SettingsView.jsx +281 -8
- package/web/src/components/settings/SettingsView.module.css +174 -0
- package/web/src/components/update/UpdateChip.jsx +104 -0
- package/web/src/components/update/UpdateChip.module.css +146 -0
- package/web/src/global.css +15 -0
- package/web/src/hooks/urlState.constants.js +25 -0
- package/web/src/hooks/useChangelog.js +51 -0
- package/web/src/hooks/useSocket.js +3 -0
- package/web/src/hooks/useUpdateChecker.js +91 -0
- package/web/src/hooks/useUrlState.js +122 -62
- package/web/dist/assets/index-BoWmUveV.js +0 -39
|
@@ -1,72 +1,152 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file web/src/components/palette/CommandPalette.jsx
|
|
3
|
-
* @description ⌘K / Ctrl+P command palette —
|
|
4
|
-
* 📖 M1
|
|
5
|
-
* 📖
|
|
6
|
-
* 📖
|
|
7
|
-
* 📖
|
|
3
|
+
* @description ⌘K / Ctrl+P command palette — M2 full version, fed by the TUI registry.
|
|
4
|
+
* 📖 M1 shipped a placeholder; this version pulls entries from
|
|
5
|
+
* 📖 `src/tui/command-palette.js` (the TUI's source of truth) so the Web and
|
|
6
|
+
* 📖 TUI palettes always show the same set of commands. Web-only commands
|
|
7
|
+
* 📖 (open modal pages, set the theme, open the help / changelog / etc.) are
|
|
8
|
+
* 📖 appended on top of the TUI list.
|
|
9
|
+
*
|
|
10
|
+
* 📖 The palette is a flat searchable list — the TUI's hierarchical category
|
|
11
|
+
* 📖 tree is preserved as a leading "section" label per entry. We fuzzy-search
|
|
12
|
+
* 📖 using the TUI's `filterCommandPaletteEntries()` so ranking is identical
|
|
13
|
+
* 📖 across both surfaces.
|
|
8
14
|
*
|
|
9
15
|
* @functions
|
|
10
|
-
* → CommandPalette
|
|
16
|
+
* → CommandPalette → main modal component
|
|
11
17
|
*/
|
|
12
|
-
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
13
|
-
import { IconSearch, IconCommand, IconBolt,
|
|
18
|
+
import { useEffect, useMemo, useRef, useState, useCallback } from 'react'
|
|
19
|
+
import { IconSearch, IconCommand, IconBolt, IconArrowsExchange, IconExternalLink } from '@tabler/icons-react'
|
|
20
|
+
import { buildCommandPaletteEntries, filterCommandPaletteEntries } from '../../../../src/tui/command-palette.js'
|
|
14
21
|
import styles from './CommandPalette.module.css'
|
|
15
22
|
|
|
23
|
+
const SECTION_META = {
|
|
24
|
+
// TUI categories → emoji + label
|
|
25
|
+
'filter': { icon: '🔍', label: 'Filter' },
|
|
26
|
+
'sort': { icon: '📶', label: 'Sort' },
|
|
27
|
+
'action': { icon: '⚙️', label: 'Action' },
|
|
28
|
+
'page': { icon: '📄', label: 'Page' },
|
|
29
|
+
'update': { icon: '⬆️', label: 'Update' },
|
|
30
|
+
// TUI 'tool' sub-categories
|
|
31
|
+
'tool': { icon: '🧰', label: 'Tool' },
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
const PING_MODE_CYCLE = ['speed', 'normal', 'slow', 'forced']
|
|
17
35
|
|
|
36
|
+
// 📖 Web-only entries (the Web has modals, not TUI overlays, so the palette
|
|
37
|
+
// 📖 has a few commands the TUI doesn't). They are appended to the TUI registry
|
|
38
|
+
// 📖 after a `Web` separator so the two surfaces stay easy to compare.
|
|
39
|
+
function buildWebEntries(deps) {
|
|
40
|
+
const { onCycleTheme, onResetView, onSetPingMode, onOpenChangelog, theme, pingMode, modelsCount, onExport } = deps
|
|
41
|
+
const web = [
|
|
42
|
+
// Pages (open as modals)
|
|
43
|
+
{ id: 'page.help', section: 'page', label: 'Open Help', keywords: ['help', 'shortcuts', 'reference'], run: () => deps.onOpenHelp?.() },
|
|
44
|
+
{ id: 'page.changelog', section: 'page', label: 'Open Changelog', keywords: ['changelog', 'release', 'history', 'version'], run: () => onOpenChangelog?.() },
|
|
45
|
+
{ id: 'page.install-endpoints', section: 'page', label: 'Open Install Endpoints (M4)', keywords: ['install', 'endpoint', 'tool', 'configure'], disabled: true },
|
|
46
|
+
{ id: 'page.installed-models', section: 'page', label: 'Open Installed Models (M4)', keywords: ['installed', 'models', 'tools'], disabled: true },
|
|
47
|
+
|
|
48
|
+
// Theme
|
|
49
|
+
{ id: 'action.theme.cycle', section: 'action', label: `Cycle theme (current: ${theme})`, keywords: ['theme', 'dark', 'light', 'auto'], run: onCycleTheme },
|
|
50
|
+
|
|
51
|
+
// Reset
|
|
52
|
+
{ id: 'action.reset.view', section: 'action', label: 'Reset view (filters + sort)', keywords: ['reset', 'view', 'clear', 'filters', 'sort'], run: onResetView },
|
|
53
|
+
|
|
54
|
+
// Ping mode (the TUI has these too; Web keeps them for parity)
|
|
55
|
+
{ id: 'action.ping.speed', section: 'action', label: 'Ping mode → Speed (2s)', keywords: ['ping', 'mode', 'speed', 'fast', '2s'], run: () => onSetPingMode?.('speed') },
|
|
56
|
+
{ id: 'action.ping.normal', section: 'action', label: 'Ping mode → Normal (10s)', keywords: ['ping', 'mode', 'normal', '10s', 'default'], run: () => onSetPingMode?.('normal') },
|
|
57
|
+
{ id: 'action.ping.slow', section: 'action', label: 'Ping mode → Slow (30s)', keywords: ['ping', 'mode', 'slow', '30s', 'idle'], run: () => onSetPingMode?.('slow') },
|
|
58
|
+
{ id: 'action.ping.forced', section: 'action', label: 'Ping mode → Forced (4s)', keywords: ['ping', 'mode', 'forced', '4s'], run: () => onSetPingMode?.('forced') },
|
|
59
|
+
|
|
60
|
+
// Export
|
|
61
|
+
{ id: 'action.export', section: 'action', label: 'Export models…', keywords: ['export', 'download', 'json', 'csv', 'clipboard'], run: onExport },
|
|
62
|
+
]
|
|
63
|
+
// TUI palette entries with a 'page' type → route through the same Web pages.
|
|
64
|
+
return web
|
|
65
|
+
}
|
|
66
|
+
|
|
18
67
|
export default function CommandPalette({
|
|
19
68
|
onClose, onNavigate, onCycleTheme, onResetView,
|
|
20
|
-
onSetPingMode,
|
|
21
|
-
currentView, theme, pingMode,
|
|
69
|
+
onSetPingMode, onOpenHelp, onOpenChangelog, onOpenCommandPalette,
|
|
70
|
+
onToast, onExport, currentView, theme, pingMode, models,
|
|
71
|
+
updateAvailable, latestVersion, onRunUpdate,
|
|
22
72
|
}) {
|
|
23
73
|
const [query, setQuery] = useState('')
|
|
24
74
|
const [cursor, setCursor] = useState(0)
|
|
25
75
|
const inputRef = useRef(null)
|
|
26
76
|
const listRef = useRef(null)
|
|
27
77
|
|
|
28
|
-
// 📖
|
|
29
|
-
// 📖
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
78
|
+
// 📖 Build the combined list once per render. TUI registry first (so
|
|
79
|
+
// 📖 its section labels come through), then Web-only entries.
|
|
80
|
+
const allCommands = useMemo(() => {
|
|
81
|
+
const tuiEntries = buildCommandPaletteEntries(models || []).map((entry) => {
|
|
82
|
+
// 📖 The TUI palette doesn't carry a 'section' field; derive one from
|
|
83
|
+
// 📖 the entry id so the Web list can show a category label.
|
|
84
|
+
let section = 'action'
|
|
85
|
+
if (entry.id.startsWith('filter-')) section = 'filter'
|
|
86
|
+
else if (entry.id.startsWith('sort-')) section = 'sort'
|
|
87
|
+
else if (entry.id.startsWith('action-')) section = 'action'
|
|
88
|
+
else if (entry.id.startsWith('action-set-tool-')) section = 'tool'
|
|
89
|
+
else if (entry.id.startsWith('page-') || entry.id === 'open-settings' || entry.id === 'open-help' || entry.id === 'open-changelog') section = 'page'
|
|
90
|
+
return { ...entry, section }
|
|
91
|
+
})
|
|
92
|
+
const webEntries = buildWebEntries({
|
|
93
|
+
onCycleTheme, onResetView, onSetPingMode,
|
|
94
|
+
onOpenChangelog, onOpenHelp, theme, pingMode, modelsCount: models?.length ?? 0, onExport,
|
|
95
|
+
})
|
|
96
|
+
// 📖 Update banner entry (only when an update is available) — mirrors
|
|
97
|
+
// 📖 the TUI palette's "auto-prepended when newer version known" rule.
|
|
98
|
+
const updateEntries = []
|
|
99
|
+
if (updateAvailable && latestVersion) {
|
|
100
|
+
updateEntries.push({
|
|
101
|
+
id: 'action.update.run', section: 'update',
|
|
102
|
+
label: `⬆️ UPDATE NOW — v${latestVersion} available (recommended!)`,
|
|
103
|
+
keywords: ['update', 'upgrade', 'version', 'install', 'new'],
|
|
104
|
+
run: () => onRunUpdate?.(),
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
return [...updateEntries, ...tuiEntries, ...webEntries]
|
|
108
|
+
}, [models, theme, pingMode, onCycleTheme, onResetView, onSetPingMode, onOpenChangelog, onOpenHelp, onExport, updateAvailable, latestVersion, onRunUpdate])
|
|
109
|
+
|
|
110
|
+
// 📖 Filter using the TUI's exact fuzzy rank so the two palettes are 1:1.
|
|
111
|
+
// 📖 Empty query → return everything; non-empty → return ranked matches.
|
|
52
112
|
const filtered = useMemo(() => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
113
|
+
if (!query.trim()) return allCommands
|
|
114
|
+
return filterCommandPaletteEntries(allCommands, query)
|
|
115
|
+
}, [allCommands, query])
|
|
116
|
+
|
|
117
|
+
// 📖 Group filtered commands by section so the list reads top-down in
|
|
118
|
+
// 📖 the same order as the TUI's hierarchical view.
|
|
119
|
+
const grouped = useMemo(() => {
|
|
120
|
+
const groups = new Map()
|
|
121
|
+
for (const cmd of filtered) {
|
|
122
|
+
if (!groups.has(cmd.section)) groups.set(cmd.section, [])
|
|
123
|
+
groups.get(cmd.section).push(cmd)
|
|
124
|
+
}
|
|
125
|
+
return Array.from(groups.entries())
|
|
126
|
+
}, [filtered])
|
|
127
|
+
|
|
128
|
+
// 📖 Build a flat list for keyboard navigation. Cursor is an index into
|
|
129
|
+
// 📖 this flat list, not the grouped structure.
|
|
130
|
+
const flatList = useMemo(() => grouped.flatMap(([, items]) => items), [grouped])
|
|
57
131
|
|
|
58
|
-
// 📖 Focus the search input on open.
|
|
59
132
|
useEffect(() => {
|
|
60
133
|
inputRef.current?.focus()
|
|
61
134
|
}, [])
|
|
62
135
|
|
|
63
|
-
// 📖
|
|
136
|
+
// 📖 Clamp cursor whenever the result set changes.
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (cursor >= flatList.length) setCursor(0)
|
|
139
|
+
}, [flatList, cursor])
|
|
140
|
+
|
|
141
|
+
// 📖 Keyboard handler: Esc, arrows, Enter, Tab (expand/collapse on TUI;
|
|
142
|
+
// 📖 the Web list is flat so Tab cycles focus to the next button — and
|
|
143
|
+
// 📖 Enter runs the highlighted command).
|
|
64
144
|
useEffect(() => {
|
|
65
145
|
const onKey = (e) => {
|
|
66
146
|
if (e.key === 'Escape') { e.preventDefault(); onClose(); return }
|
|
67
147
|
if (e.key === 'ArrowDown') {
|
|
68
148
|
e.preventDefault()
|
|
69
|
-
setCursor((c) => Math.min(
|
|
149
|
+
setCursor((c) => Math.min(flatList.length - 1, c + 1))
|
|
70
150
|
return
|
|
71
151
|
}
|
|
72
152
|
if (e.key === 'ArrowUp') {
|
|
@@ -76,21 +156,77 @@ export default function CommandPalette({
|
|
|
76
156
|
}
|
|
77
157
|
if (e.key === 'Enter') {
|
|
78
158
|
e.preventDefault()
|
|
79
|
-
const item =
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} else if (item?.disabled) {
|
|
84
|
-
onToast?.(`${item.label} is not available yet.`, 'info')
|
|
85
|
-
}
|
|
159
|
+
const item = flatList[cursor]
|
|
160
|
+
if (!item) return
|
|
161
|
+
if (item.disabled) { onToast?.(`${item.label} is not available yet.`, 'info'); return }
|
|
162
|
+
runCommand(item)
|
|
86
163
|
}
|
|
87
164
|
}
|
|
88
165
|
document.addEventListener('keydown', onKey)
|
|
89
166
|
return () => document.removeEventListener('keydown', onKey)
|
|
90
|
-
|
|
167
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
168
|
+
}, [flatList, cursor, onClose, onToast])
|
|
169
|
+
|
|
170
|
+
// 📖 When a command runs, the palette closes. The handler lives in a
|
|
171
|
+
// 📖 useCallback so the keyboard handler doesn't recreate on every render.
|
|
172
|
+
const runCommand = useCallback((item) => {
|
|
173
|
+
// 📖 TUI registry entries carry the action in their own `id` form. We
|
|
174
|
+
// 📖 translate the most common ones to the Web's React callbacks.
|
|
175
|
+
// 📖 For entries that don't have a Web equivalent (e.g. tool cycle, which
|
|
176
|
+
// 📖 ships in M3), we route the user to the right header menu.
|
|
177
|
+
const id = item.id
|
|
178
|
+
if (id === 'open-settings') { onNavigate?.('settings'); onClose(); return }
|
|
179
|
+
if (id === 'open-help') { onOpenHelp?.(); onClose(); return }
|
|
180
|
+
if (id === 'open-changelog') { onOpenChangelog?.(); onClose(); return }
|
|
181
|
+
if (id === 'open-recommend') { onToast?.('Recommend arrives in M3', 'info'); onClose(); return }
|
|
182
|
+
if (id === 'open-router-dashboard') { onToast?.('Router dashboard arrives in M4', 'info'); onClose(); return }
|
|
183
|
+
if (id === 'open-installed-models') { onToast?.('Installed Models arrives in M4', 'info'); onClose(); return }
|
|
184
|
+
if (id === 'open-install-endpoints') { onToast?.('Install Endpoints arrives in M4', 'info'); onClose(); return }
|
|
185
|
+
if (id === 'action-update-now') { onRunUpdate?.(); onClose(); return }
|
|
186
|
+
if (id === 'action-cycle-theme') { onCycleTheme?.(); onClose(); return }
|
|
187
|
+
if (id === 'action-reset-view') { onResetView?.(); onClose(); return }
|
|
188
|
+
if (id === 'action-cycle-ping-mode') {
|
|
189
|
+
const idx = PING_MODE_CYCLE.indexOf(pingMode)
|
|
190
|
+
onSetPingMode?.(PING_MODE_CYCLE[(idx + 1) % PING_MODE_CYCLE.length])
|
|
191
|
+
onClose()
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
if (id === 'action-cycle-tool-mode') { onToast?.('Tool mode picker arrives in M3', 'info'); onClose(); return }
|
|
195
|
+
if (id === 'action-toggle-favorite') { onToast?.('Press F on a model row in the table.', 'info'); onClose(); return }
|
|
196
|
+
if (id === 'action-toggle-favorite-mode') { onToast?.('Set the favorites display mode in Settings (M2).', 'info'); onClose(); return }
|
|
197
|
+
if (id === 'action-export') { onExport?.(); onClose(); return }
|
|
198
|
+
if (id === 'action-benchmark-row') { onToast?.('Click any AI Lat. cell to benchmark the highlighted row.', 'info'); onClose(); return }
|
|
91
199
|
|
|
92
|
-
|
|
93
|
-
|
|
200
|
+
// 📖 Filter / sort / ping mode commands are dispatched to the same Web
|
|
201
|
+
// 📖 callbacks the TUI palette uses (cycle / set). For per-model filter
|
|
202
|
+
// 📖 entries the user typed, we just apply a text filter.
|
|
203
|
+
if (id === 'filter-provider-cycle') { onToast?.('Use the Provider dropdown in the FilterBar.', 'info'); onClose(); return }
|
|
204
|
+
if (id.startsWith('filter-tier-')) { onToast?.('Tier filter — use the Tier chip row in the FilterBar.', 'info'); onClose(); return }
|
|
205
|
+
if (id.startsWith('filter-provider-')) { onToast?.('Provider filter — use the Provider dropdown.', 'info'); onClose(); return }
|
|
206
|
+
if (id === 'filter-configured-toggle') { onToast?.('Use the Visibility dropdown in the FilterBar.', 'info'); onClose(); return }
|
|
207
|
+
if (id.startsWith('sort-')) { onToast?.('Sort — click the column header in the table.', 'info'); onClose(); return }
|
|
208
|
+
if (id === 'action-set-ping-speed') { onSetPingMode?.('speed'); onClose(); return }
|
|
209
|
+
if (id === 'action-set-ping-normal') { onSetPingMode?.('normal'); onClose(); return }
|
|
210
|
+
if (id === 'action-set-ping-slow') { onSetPingMode?.('slow'); onClose(); return }
|
|
211
|
+
if (id === 'action-set-ping-forced') { onSetPingMode?.('forced'); onClose(); return }
|
|
212
|
+
|
|
213
|
+
// 📖 Fallback for any TUI command the Web hasn't wired yet.
|
|
214
|
+
if (typeof item.run === 'function') {
|
|
215
|
+
item.run()
|
|
216
|
+
onClose()
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
onToast?.(`${item.label} is not wired on the Web yet.`, 'info')
|
|
220
|
+
onClose()
|
|
221
|
+
}, [onNavigate, onClose, onToast, onCycleTheme, onResetView, onSetPingMode, onOpenHelp, onOpenChangelog, onRunUpdate, onExport, pingMode])
|
|
222
|
+
|
|
223
|
+
// 📖 Keep the focused item in view as the cursor moves. Mirrors the TUI
|
|
224
|
+
// 📖 palette's "follow the selection" behavior.
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (!listRef.current) return
|
|
227
|
+
const el = listRef.current.querySelector(`[data-cmd-index="${cursor}"]`)
|
|
228
|
+
if (el) el.scrollIntoView({ block: 'nearest' })
|
|
229
|
+
}, [cursor])
|
|
94
230
|
|
|
95
231
|
return (
|
|
96
232
|
<div className={styles.backdrop} onClick={onClose}>
|
|
@@ -100,7 +236,7 @@ export default function CommandPalette({
|
|
|
100
236
|
<input
|
|
101
237
|
ref={inputRef}
|
|
102
238
|
className={styles.input}
|
|
103
|
-
placeholder="Type a command…"
|
|
239
|
+
placeholder="Type a command… (filters, sorts, tools, theme, ping, export)"
|
|
104
240
|
value={query}
|
|
105
241
|
onChange={(e) => setQuery(e.target.value)}
|
|
106
242
|
autoComplete="off"
|
|
@@ -108,37 +244,55 @@ export default function CommandPalette({
|
|
|
108
244
|
<span className={styles.kbd}><IconCommand size={10} stroke={1.5} />K</span>
|
|
109
245
|
</div>
|
|
110
246
|
|
|
111
|
-
<
|
|
112
|
-
{
|
|
113
|
-
<
|
|
247
|
+
<div className={styles.list} ref={listRef} role="listbox">
|
|
248
|
+
{flatList.length === 0 && (
|
|
249
|
+
<div className={styles.empty}>No matching command.</div>
|
|
114
250
|
)}
|
|
115
|
-
{
|
|
116
|
-
const
|
|
251
|
+
{grouped.map(([section, items]) => {
|
|
252
|
+
const meta = SECTION_META[section] || { icon: '•', label: section }
|
|
117
253
|
return (
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
254
|
+
<div key={section} className={styles.section}>
|
|
255
|
+
<div className={styles.sectionHeader}>
|
|
256
|
+
<span className={styles.sectionIcon}>{meta.icon}</span>
|
|
257
|
+
<span>{meta.label}</span>
|
|
258
|
+
</div>
|
|
259
|
+
{items.map((cmd) => {
|
|
260
|
+
// 📖 Flat index across all groups so the keyboard cursor is stable.
|
|
261
|
+
const flatIdx = flatList.indexOf(cmd)
|
|
262
|
+
const isActive = flatIdx === cursor
|
|
263
|
+
return (
|
|
264
|
+
<button
|
|
265
|
+
key={cmd.id}
|
|
266
|
+
data-cmd-index={flatIdx}
|
|
267
|
+
className={`${styles.item} ${isActive ? styles.itemActive : ''} ${cmd.disabled ? styles.itemDisabled : ''}`}
|
|
268
|
+
onClick={() => runCommand(cmd)}
|
|
269
|
+
onMouseEnter={() => setCursor(flatIdx)}
|
|
270
|
+
role="option"
|
|
271
|
+
aria-selected={isActive}
|
|
272
|
+
aria-disabled={cmd.disabled ? 'true' : undefined}
|
|
273
|
+
>
|
|
274
|
+
<span className={styles.itemLabel}>
|
|
275
|
+
{cmd.label}
|
|
276
|
+
{cmd.shortcut && <span className={styles.shortcut}>{cmd.shortcut}</span>}
|
|
277
|
+
</span>
|
|
278
|
+
{cmd.description && <span className={styles.itemDesc}>{cmd.description}</span>}
|
|
279
|
+
{isActive && <span className={styles.itemEnter}>↵</span>}
|
|
280
|
+
</button>
|
|
281
|
+
)
|
|
282
|
+
})}
|
|
283
|
+
</div>
|
|
134
284
|
)
|
|
135
285
|
})}
|
|
136
|
-
</
|
|
286
|
+
</div>
|
|
137
287
|
|
|
138
288
|
<div className={styles.footer}>
|
|
139
289
|
<span><kbd>↑↓</kbd> navigate</span>
|
|
140
290
|
<span><kbd>↵</kbd> select</span>
|
|
141
291
|
<span><kbd>Esc</kbd> close</span>
|
|
292
|
+
<span className={styles.footerRight}>
|
|
293
|
+
Powered by the TUI command registry — 1:1 parity
|
|
294
|
+
<IconExternalLink size={10} stroke={1.5} style={{ marginLeft: 4 }} />
|
|
295
|
+
</span>
|
|
142
296
|
</div>
|
|
143
297
|
</div>
|
|
144
298
|
</div>
|