@makefinks/daemon 0.1.0

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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/dist/cli.js +22 -0
  4. package/package.json +79 -0
  5. package/src/ai/agent-turn-runner.ts +130 -0
  6. package/src/ai/daemon-ai.ts +403 -0
  7. package/src/ai/exa-client.ts +21 -0
  8. package/src/ai/exa-fetch-cache.ts +104 -0
  9. package/src/ai/model-config.ts +99 -0
  10. package/src/ai/sanitize-messages.ts +83 -0
  11. package/src/ai/system-prompt.ts +363 -0
  12. package/src/ai/tools/fetch-urls.ts +187 -0
  13. package/src/ai/tools/grounding-manager.ts +94 -0
  14. package/src/ai/tools/index.ts +52 -0
  15. package/src/ai/tools/read-file.ts +100 -0
  16. package/src/ai/tools/render-url.ts +275 -0
  17. package/src/ai/tools/run-bash.ts +224 -0
  18. package/src/ai/tools/subagents.ts +195 -0
  19. package/src/ai/tools/todo-manager.ts +150 -0
  20. package/src/ai/tools/web-search.ts +91 -0
  21. package/src/app/App.tsx +711 -0
  22. package/src/app/components/AppOverlays.tsx +131 -0
  23. package/src/app/components/AvatarLayer.tsx +51 -0
  24. package/src/app/components/ConversationPane.tsx +476 -0
  25. package/src/avatar/DaemonAvatarRenderable.ts +343 -0
  26. package/src/avatar/daemon-avatar-rig.ts +1165 -0
  27. package/src/avatar-preview.ts +186 -0
  28. package/src/cli.ts +26 -0
  29. package/src/components/ApiKeyInput.tsx +99 -0
  30. package/src/components/ApiKeyStep.tsx +95 -0
  31. package/src/components/ApprovalPicker.tsx +109 -0
  32. package/src/components/ContentBlockView.tsx +141 -0
  33. package/src/components/DaemonText.tsx +34 -0
  34. package/src/components/DeviceMenu.tsx +166 -0
  35. package/src/components/GroundingBadge.tsx +21 -0
  36. package/src/components/GroundingMenu.tsx +310 -0
  37. package/src/components/HotkeysPane.tsx +115 -0
  38. package/src/components/InlineStatusIndicator.tsx +106 -0
  39. package/src/components/ModelMenu.tsx +411 -0
  40. package/src/components/OnboardingOverlay.tsx +446 -0
  41. package/src/components/ProviderMenu.tsx +177 -0
  42. package/src/components/SessionMenu.tsx +297 -0
  43. package/src/components/SettingsMenu.tsx +291 -0
  44. package/src/components/StatusBar.tsx +126 -0
  45. package/src/components/TokenUsageDisplay.tsx +92 -0
  46. package/src/components/ToolCallView.tsx +113 -0
  47. package/src/components/TypingInputBar.tsx +131 -0
  48. package/src/components/tool-layouts/components.tsx +120 -0
  49. package/src/components/tool-layouts/defaults.ts +9 -0
  50. package/src/components/tool-layouts/index.ts +22 -0
  51. package/src/components/tool-layouts/layouts/bash.ts +110 -0
  52. package/src/components/tool-layouts/layouts/grounding.tsx +98 -0
  53. package/src/components/tool-layouts/layouts/index.ts +8 -0
  54. package/src/components/tool-layouts/layouts/read-file.ts +59 -0
  55. package/src/components/tool-layouts/layouts/subagent.tsx +118 -0
  56. package/src/components/tool-layouts/layouts/system-info.ts +8 -0
  57. package/src/components/tool-layouts/layouts/todo.tsx +139 -0
  58. package/src/components/tool-layouts/layouts/url-tools.ts +220 -0
  59. package/src/components/tool-layouts/layouts/web-search.ts +110 -0
  60. package/src/components/tool-layouts/registry.ts +17 -0
  61. package/src/components/tool-layouts/types.ts +94 -0
  62. package/src/hooks/daemon-event-handlers.ts +944 -0
  63. package/src/hooks/keyboard-handlers.ts +399 -0
  64. package/src/hooks/menu-navigation.ts +147 -0
  65. package/src/hooks/use-app-audio-devices-loader.ts +71 -0
  66. package/src/hooks/use-app-callbacks.ts +202 -0
  67. package/src/hooks/use-app-context-builder.ts +159 -0
  68. package/src/hooks/use-app-display-state.ts +162 -0
  69. package/src/hooks/use-app-menus.ts +51 -0
  70. package/src/hooks/use-app-model-pricing-loader.ts +45 -0
  71. package/src/hooks/use-app-model.ts +123 -0
  72. package/src/hooks/use-app-openrouter-models-loader.ts +44 -0
  73. package/src/hooks/use-app-openrouter-provider-loader.ts +35 -0
  74. package/src/hooks/use-app-preferences-bootstrap.ts +212 -0
  75. package/src/hooks/use-app-sessions.ts +105 -0
  76. package/src/hooks/use-app-settings.ts +62 -0
  77. package/src/hooks/use-conversation-manager.ts +163 -0
  78. package/src/hooks/use-copy-on-select.ts +50 -0
  79. package/src/hooks/use-daemon-events.ts +396 -0
  80. package/src/hooks/use-daemon-keyboard.ts +397 -0
  81. package/src/hooks/use-grounding.ts +46 -0
  82. package/src/hooks/use-input-history.ts +92 -0
  83. package/src/hooks/use-menu-keyboard.ts +93 -0
  84. package/src/hooks/use-playwright-notification.ts +23 -0
  85. package/src/hooks/use-reasoning-animation.ts +97 -0
  86. package/src/hooks/use-response-timer.ts +55 -0
  87. package/src/hooks/use-tool-approval.tsx +202 -0
  88. package/src/hooks/use-typing-mode.ts +137 -0
  89. package/src/hooks/use-voice-dependencies-notification.ts +37 -0
  90. package/src/index.tsx +48 -0
  91. package/src/scripts/setup-browsers.ts +42 -0
  92. package/src/state/app-context.tsx +160 -0
  93. package/src/state/daemon-events.ts +67 -0
  94. package/src/state/daemon-state.ts +493 -0
  95. package/src/state/migrations/001-init.ts +33 -0
  96. package/src/state/migrations/index.ts +8 -0
  97. package/src/state/model-history-store.ts +45 -0
  98. package/src/state/runtime-context.ts +21 -0
  99. package/src/state/session-store.ts +359 -0
  100. package/src/types/index.ts +405 -0
  101. package/src/types/theme.ts +52 -0
  102. package/src/ui/constants.ts +157 -0
  103. package/src/utils/clipboard.ts +89 -0
  104. package/src/utils/debug-logger.ts +69 -0
  105. package/src/utils/formatters.ts +242 -0
  106. package/src/utils/js-rendering.ts +77 -0
  107. package/src/utils/markdown-tables.ts +234 -0
  108. package/src/utils/model-metadata.ts +191 -0
  109. package/src/utils/openrouter-endpoints.ts +212 -0
  110. package/src/utils/openrouter-models.ts +205 -0
  111. package/src/utils/openrouter-pricing.ts +59 -0
  112. package/src/utils/openrouter-reported-cost.ts +16 -0
  113. package/src/utils/paste.ts +33 -0
  114. package/src/utils/preferences.ts +289 -0
  115. package/src/utils/text-fragment.ts +39 -0
  116. package/src/utils/tool-output-preview.ts +250 -0
  117. package/src/utils/voice-dependencies.ts +107 -0
  118. package/src/utils/workspace-manager.ts +85 -0
  119. package/src/voice/audio-recorder.ts +579 -0
  120. package/src/voice/mic-level.ts +35 -0
  121. package/src/voice/tts/openai-tts-stream.ts +222 -0
  122. package/src/voice/tts/speech-controller.ts +64 -0
  123. package/src/voice/tts/tts-player.ts +257 -0
  124. package/src/voice/voice-input-controller.ts +96 -0
@@ -0,0 +1,411 @@
1
+ import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core";
2
+ import { useKeyboard } from "@opentui/react";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import type { ModelOption } from "../types";
5
+ import { useMenuKeyboard } from "../hooks/use-menu-keyboard";
6
+ import { COLORS } from "../ui/constants";
7
+ import { formatContextWindowK, formatPrice } from "../utils/formatters";
8
+
9
+ const COL_WIDTH = {
10
+ CTX: 6,
11
+ IN: 10,
12
+ OUT: 10,
13
+ CACHE: 6,
14
+ } as const;
15
+
16
+ const ALL_MODEL_ITEM_HEIGHT = 1;
17
+ const MAX_ALL_SCROLLBOX_HEIGHT = 20;
18
+ const MIN_ALL_MODEL_QUERY_LENGTH = 3;
19
+
20
+ interface ModelMenuProps {
21
+ curatedModels: ModelOption[];
22
+ allModels: ModelOption[];
23
+ allModelsLoading: boolean;
24
+ allModelsUpdatedAt: number | null;
25
+ currentModelId: string;
26
+ onClose: () => void;
27
+ onSelect: (model: ModelOption) => void;
28
+ onRefreshAllModels: () => void;
29
+ }
30
+
31
+ function formatUpdatedAt(timestamp: number | null): string {
32
+ if (!timestamp) return "";
33
+ return new Date(timestamp).toISOString().replace("T", " ").slice(0, 16);
34
+ }
35
+
36
+ export function ModelMenu({
37
+ curatedModels,
38
+ allModels,
39
+ allModelsLoading,
40
+ allModelsUpdatedAt,
41
+ currentModelId,
42
+ onClose,
43
+ onSelect,
44
+ onRefreshAllModels,
45
+ }: ModelMenuProps) {
46
+ const [searchQuery, setSearchQuery] = useState("");
47
+ const [isSearchFocused, setIsSearchFocused] = useState(false);
48
+ const searchInputRef = useRef<TextareaRenderable | null>(null);
49
+
50
+ const sortedCurated = useMemo(() => {
51
+ return [...curatedModels].sort((a, b) => {
52
+ const priceA = a.pricing ? a.pricing.prompt + a.pricing.completion : Number.MAX_SAFE_INTEGER;
53
+ const priceB = b.pricing ? b.pricing.prompt + b.pricing.completion : Number.MAX_SAFE_INTEGER;
54
+ if (priceA !== priceB) return priceA - priceB;
55
+ return a.name.localeCompare(b.name);
56
+ });
57
+ }, [curatedModels]);
58
+
59
+ const curatedIdSet = useMemo(() => new Set(sortedCurated.map((model) => model.id)), [sortedCurated]);
60
+
61
+ const allModelsWithFallback = useMemo(() => {
62
+ if (!currentModelId) return allModels;
63
+ if (curatedIdSet.has(currentModelId)) return allModels;
64
+ if (allModels.some((model) => model.id === currentModelId)) return allModels;
65
+ return [...allModels, { id: currentModelId, name: currentModelId }];
66
+ }, [allModels, curatedIdSet, currentModelId]);
67
+
68
+ const filteredAllModels = useMemo(() => {
69
+ const filtered = allModelsWithFallback.filter((model) => !curatedIdSet.has(model.id));
70
+ const query = searchQuery.trim().toLowerCase();
71
+ if (query.length < MIN_ALL_MODEL_QUERY_LENGTH) {
72
+ return [];
73
+ }
74
+ const matching = query
75
+ ? filtered.filter(
76
+ (model) => model.name.toLowerCase().includes(query) || model.id.toLowerCase().includes(query)
77
+ )
78
+ : filtered;
79
+
80
+ return matching.sort((a, b) => a.name.localeCompare(b.name));
81
+ }, [allModelsWithFallback, curatedIdSet, searchQuery]);
82
+
83
+ const totalItems = sortedCurated.length + filteredAllModels.length;
84
+
85
+ const initialIndex = useMemo(() => {
86
+ if (totalItems === 0) return 0;
87
+ const curatedIdx = sortedCurated.findIndex((model) => model.id === currentModelId);
88
+ if (curatedIdx >= 0) return curatedIdx;
89
+ const allIdx = filteredAllModels.findIndex((model) => model.id === currentModelId);
90
+ if (allIdx >= 0) return sortedCurated.length + allIdx;
91
+ return 0;
92
+ }, [sortedCurated, filteredAllModels, currentModelId, totalItems]);
93
+
94
+ const { selectedIndex } = useMenuKeyboard({
95
+ itemCount: totalItems,
96
+ initialIndex,
97
+ onClose,
98
+ onSelect: (selectedIdx) => {
99
+ const isCurated = selectedIdx < sortedCurated.length;
100
+ const model = isCurated
101
+ ? sortedCurated[selectedIdx]
102
+ : filteredAllModels[selectedIdx - sortedCurated.length];
103
+ if (model) {
104
+ onSelect(model);
105
+ }
106
+ },
107
+ enableViKeys: !isSearchFocused,
108
+ ignoreEscape: isSearchFocused,
109
+ });
110
+
111
+ useKeyboard((key) => {
112
+ if (key.eventType !== "press") return;
113
+
114
+ if (!isSearchFocused && (key.name === "r" || key.sequence?.toLowerCase() === "r")) {
115
+ onRefreshAllModels();
116
+ key.preventDefault();
117
+ return;
118
+ }
119
+
120
+ if ((key.name === "tab" && key.shift) || (!isSearchFocused && key.name === "/")) {
121
+ setIsSearchFocused(true);
122
+ searchInputRef.current?.focus();
123
+ key.preventDefault();
124
+ }
125
+ });
126
+
127
+ const allSelectedIndex = selectedIndex - sortedCurated.length;
128
+ const isAllSectionSelected = allSelectedIndex >= 0;
129
+
130
+ const scrollRef = useRef<ScrollBoxRenderable | null>(null);
131
+ const scrollboxHeight = Math.min(
132
+ MAX_ALL_SCROLLBOX_HEIGHT,
133
+ Math.max(ALL_MODEL_ITEM_HEIGHT, filteredAllModels.length * ALL_MODEL_ITEM_HEIGHT)
134
+ );
135
+
136
+ useEffect(() => {
137
+ if (!isAllSectionSelected) return;
138
+ const scrollbox = scrollRef.current;
139
+ if (!scrollbox) return;
140
+ const viewportHeight = scrollbox.viewport?.height ?? 0;
141
+ if (viewportHeight <= 0) return;
142
+
143
+ const maxScrollTop = Math.max(0, scrollbox.scrollHeight - viewportHeight);
144
+ const itemTop = allSelectedIndex * ALL_MODEL_ITEM_HEIGHT;
145
+ const itemBottom = itemTop + ALL_MODEL_ITEM_HEIGHT;
146
+ const currentTop = scrollbox.scrollTop;
147
+ const currentBottom = currentTop + viewportHeight;
148
+ let nextTop = currentTop;
149
+
150
+ if (itemTop < currentTop) {
151
+ nextTop = itemTop;
152
+ } else if (itemBottom > currentBottom) {
153
+ nextTop = itemBottom - viewportHeight;
154
+ }
155
+
156
+ nextTop = Math.max(0, Math.min(nextTop, maxScrollTop));
157
+ if (nextTop !== currentTop) {
158
+ scrollbox.scrollTop = nextTop;
159
+ }
160
+ }, [allSelectedIndex, filteredAllModels.length, isAllSectionSelected]);
161
+
162
+ const updatedAtLabel = formatUpdatedAt(allModelsUpdatedAt);
163
+
164
+ const renderModelRow = (model: ModelOption, isSelected: boolean, isCurrent: boolean) => {
165
+ const pricing = model.pricing;
166
+ const ctxText =
167
+ typeof model.contextLength === "number" && model.contextLength > 0
168
+ ? formatContextWindowK(model.contextLength)
169
+ : "--";
170
+
171
+ const inText = pricing ? formatPrice(pricing.prompt) : "--";
172
+ const outText = pricing ? formatPrice(pricing.completion) : "--";
173
+
174
+ const supportsCaching = Boolean(model.supportsCaching);
175
+ const cacheText = supportsCaching ? "✓" : "x";
176
+
177
+ return (
178
+ <box
179
+ key={model.id}
180
+ backgroundColor={isSelected ? COLORS.MENU_SELECTED_BG : COLORS.MENU_BG}
181
+ paddingLeft={1}
182
+ paddingRight={1}
183
+ flexDirection="row"
184
+ justifyContent="space-between"
185
+ >
186
+ <text>
187
+ <span fg={isSelected ? COLORS.DAEMON_LABEL : COLORS.MENU_TEXT}>
188
+ {isSelected ? "▶ " : " "}
189
+ {model.name}
190
+ {isCurrent ? " ●" : ""}
191
+ </span>
192
+ </text>
193
+ <text>
194
+ <span fg={COLORS.MENU_TEXT}>{ctxText.padStart(COL_WIDTH.CTX)} </span>
195
+ <span fg={COLORS.TYPING_PROMPT}>
196
+ {inText.padStart(COL_WIDTH.IN)} {outText.padStart(COL_WIDTH.OUT)}{" "}
197
+ </span>
198
+ <span fg={supportsCaching ? COLORS.DAEMON_TEXT : COLORS.REASONING_DIM}>
199
+ {cacheText.padStart(COL_WIDTH.CACHE)}
200
+ </span>
201
+ </text>
202
+ </box>
203
+ );
204
+ };
205
+
206
+ return (
207
+ <box
208
+ position="absolute"
209
+ left={0}
210
+ top={0}
211
+ width="100%"
212
+ height="100%"
213
+ flexDirection="column"
214
+ alignItems="center"
215
+ justifyContent="center"
216
+ zIndex={100}
217
+ >
218
+ <box
219
+ flexDirection="column"
220
+ backgroundColor={COLORS.MENU_BG}
221
+ borderStyle="single"
222
+ borderColor={COLORS.MENU_BORDER}
223
+ paddingLeft={2}
224
+ paddingRight={2}
225
+ paddingTop={1}
226
+ paddingBottom={1}
227
+ width="75%"
228
+ minWidth={60}
229
+ maxWidth={170}
230
+ >
231
+ <box marginBottom={1}>
232
+ <text>
233
+ <span fg={COLORS.DAEMON_LABEL}>[ MODEL SELECTION ]</span>
234
+ </text>
235
+ </box>
236
+ <box marginBottom={1}>
237
+ <text>
238
+ <span fg={COLORS.USER_LABEL}>↑/↓ or j/k navigate · ENTER select · R refresh · ESC cancel</span>
239
+ </text>
240
+ </box>
241
+ <box marginBottom={1}>
242
+ <text>
243
+ <span fg={COLORS.REASONING_DIM}>Shift+Tab or / search · Esc blur search</span>
244
+ </text>
245
+ </box>
246
+
247
+ <box marginBottom={0}>
248
+ <text>
249
+ <span fg={COLORS.USER_LABEL}>— SEARCH ALL MODELS —</span>
250
+ </text>
251
+ </box>
252
+
253
+ <box
254
+ marginBottom={1}
255
+ marginTop={0}
256
+ width="100%"
257
+ height={1}
258
+ flexDirection="row"
259
+ alignItems="center"
260
+ paddingLeft={1}
261
+ backgroundColor={isSearchFocused ? COLORS.MENU_SELECTED_BG : COLORS.MENU_BG}
262
+ >
263
+ <box width={2}>
264
+ <text>
265
+ <span fg={isSearchFocused ? COLORS.TYPING_PROMPT : COLORS.REASONING_DIM}>/ </span>
266
+ </text>
267
+ </box>
268
+ <box flexGrow={1} height={1}>
269
+ <textarea
270
+ ref={searchInputRef}
271
+ focused={isSearchFocused}
272
+ width="100%"
273
+ height={1}
274
+ placeholder="Type to filter... (/ or Shift+Tab)"
275
+ style={{
276
+ backgroundColor: "transparent",
277
+ focusedBackgroundColor: "transparent",
278
+ textColor: COLORS.MENU_TEXT,
279
+ focusedTextColor: COLORS.TYPING_PROMPT,
280
+ cursorColor: COLORS.TYPING_PROMPT,
281
+ }}
282
+ onContentChange={() => {
283
+ const text = searchInputRef.current?.plainText ?? "";
284
+ const cleaned = text.replace(/[\r\n]/g, "");
285
+ if (cleaned !== text) {
286
+ searchInputRef.current?.setText(cleaned);
287
+ }
288
+ setSearchQuery(cleaned);
289
+ }}
290
+ onKeyDown={(key) => {
291
+ if (key.eventType === "press") {
292
+ if (key.name === "escape") {
293
+ setIsSearchFocused(false);
294
+ key.preventDefault();
295
+ }
296
+ if (key.name === "return") {
297
+ key.preventDefault();
298
+ }
299
+ }
300
+ }}
301
+ />
302
+ </box>
303
+ </box>
304
+
305
+ {updatedAtLabel ? (
306
+ <box marginBottom={1}>
307
+ <text>
308
+ <span fg={COLORS.REASONING_DIM}>All models updated: {updatedAtLabel}</span>
309
+ </text>
310
+ </box>
311
+ ) : null}
312
+
313
+ <box marginBottom={1} marginTop={1}>
314
+ <text>
315
+ <span fg={COLORS.DAEMON_LABEL}>[ RECOMMENDED ]</span>
316
+ </text>
317
+ </box>
318
+
319
+ {sortedCurated.length === 0 ? (
320
+ <box marginBottom={1} paddingLeft={1}>
321
+ <text>
322
+ <span fg={COLORS.REASONING_DIM}>No curated models available</span>
323
+ </text>
324
+ </box>
325
+ ) : (
326
+ <>
327
+ <box marginBottom={1}>
328
+ <box flexDirection="row" justifyContent="space-between">
329
+ <text>
330
+ <span fg={COLORS.REASONING_DIM}>MODEL</span>
331
+ </text>
332
+ <text>
333
+ <span fg={COLORS.REASONING_DIM}>
334
+ {"CTX".padStart(COL_WIDTH.CTX)} {"IN".padStart(COL_WIDTH.IN)}{" "}
335
+ {"OUT".padStart(COL_WIDTH.OUT)} {"CACHE".padStart(COL_WIDTH.CACHE)}
336
+ </span>
337
+ </text>
338
+ </box>
339
+ </box>
340
+ <box flexDirection="column">
341
+ {sortedCurated.map((model, idx) =>
342
+ renderModelRow(model, idx === selectedIndex, model.id === currentModelId)
343
+ )}
344
+ </box>
345
+ </>
346
+ )}
347
+
348
+ <box marginBottom={1} marginTop={1}>
349
+ <text>
350
+ <span fg={COLORS.DAEMON_LABEL}>[ ALL MODELS ]</span>
351
+ {allModelsLoading ? <span fg={COLORS.REASONING_DIM}> (refreshing...)</span> : null}
352
+ </text>
353
+ </box>
354
+
355
+ {filteredAllModels.length === 0 ? (
356
+ <box marginTop={0} paddingLeft={1}>
357
+ <text>
358
+ <span fg={COLORS.REASONING_DIM}>
359
+ {searchQuery.trim().length < MIN_ALL_MODEL_QUERY_LENGTH
360
+ ? `Type ${MIN_ALL_MODEL_QUERY_LENGTH}+ characters to search`
361
+ : allModelsLoading
362
+ ? "Loading models..."
363
+ : "No models found"}
364
+ </span>
365
+ </text>
366
+ </box>
367
+ ) : (
368
+ <>
369
+ <box marginBottom={1}>
370
+ <box flexDirection="row" justifyContent="space-between">
371
+ <text>
372
+ <span fg={COLORS.REASONING_DIM}>MODEL</span>
373
+ </text>
374
+ <text>
375
+ <span fg={COLORS.REASONING_DIM}>
376
+ {"CTX".padStart(COL_WIDTH.CTX)} {"IN".padStart(COL_WIDTH.IN)}{" "}
377
+ {"OUT".padStart(COL_WIDTH.OUT)} {"CACHE".padStart(COL_WIDTH.CACHE)}
378
+ </span>
379
+ </text>
380
+ </box>
381
+ </box>
382
+ <scrollbox
383
+ ref={scrollRef}
384
+ height={scrollboxHeight}
385
+ alignSelf="flex-start"
386
+ focused={false}
387
+ scrollY={true}
388
+ scrollX={false}
389
+ style={{
390
+ rootOptions: { backgroundColor: COLORS.MENU_BG },
391
+ wrapperOptions: { backgroundColor: COLORS.MENU_BG },
392
+ viewportOptions: { backgroundColor: COLORS.MENU_BG },
393
+ contentOptions: { backgroundColor: COLORS.MENU_BG },
394
+ }}
395
+ >
396
+ <box flexDirection="column">
397
+ {filteredAllModels.map((model, idx) =>
398
+ renderModelRow(
399
+ model,
400
+ sortedCurated.length + idx === selectedIndex,
401
+ model.id === currentModelId
402
+ )
403
+ )}
404
+ </box>
405
+ </scrollbox>
406
+ </>
407
+ )}
408
+ </box>
409
+ </box>
410
+ );
411
+ }