claudeup 4.6.1 → 4.8.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 (49) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/gap-fill-versions.test.ts +382 -0
  3. package/src/data/settings-catalog.js +2 -7
  4. package/src/data/settings-catalog.ts +2 -7
  5. package/src/opentui.d.ts +7 -2
  6. package/src/prerunner/index.js +31 -17
  7. package/src/prerunner/index.ts +35 -18
  8. package/src/services/claude-settings.js +74 -0
  9. package/src/services/claude-settings.ts +92 -0
  10. package/src/services/plugin-manager.js +13 -16
  11. package/src/services/plugin-manager.ts +17 -16
  12. package/src/services/settings-manager.js +84 -5
  13. package/src/services/settings-manager.ts +86 -5
  14. package/src/ui/adapters/settingsAdapter.js +8 -8
  15. package/src/ui/adapters/settingsAdapter.ts +8 -8
  16. package/src/ui/components/TabBar.js +1 -23
  17. package/src/ui/components/TabBar.tsx +1 -26
  18. package/src/ui/components/modals/ConfirmModal.js +1 -1
  19. package/src/ui/components/modals/ConfirmModal.tsx +17 -16
  20. package/src/ui/components/modals/InputModal.js +2 -13
  21. package/src/ui/components/modals/InputModal.tsx +21 -24
  22. package/src/ui/components/modals/LoadingModal.js +1 -1
  23. package/src/ui/components/modals/LoadingModal.tsx +6 -6
  24. package/src/ui/components/modals/MessageModal.js +4 -4
  25. package/src/ui/components/modals/MessageModal.tsx +13 -13
  26. package/src/ui/components/modals/ModalContainer.js +25 -2
  27. package/src/ui/components/modals/ModalContainer.tsx +25 -2
  28. package/src/ui/components/modals/SelectModal.js +3 -4
  29. package/src/ui/components/modals/SelectModal.tsx +18 -15
  30. package/src/ui/renderers/settingsRenderers.js +1 -1
  31. package/src/ui/renderers/settingsRenderers.tsx +5 -3
  32. package/src/ui/screens/CliToolsScreen.js +2 -2
  33. package/src/ui/screens/CliToolsScreen.tsx +3 -1
  34. package/src/ui/screens/EnvVarsScreen.js +27 -10
  35. package/src/ui/screens/EnvVarsScreen.tsx +33 -16
  36. package/src/ui/screens/McpRegistryScreen.js +2 -2
  37. package/src/ui/screens/McpRegistryScreen.tsx +3 -1
  38. package/src/ui/screens/McpScreen.js +1 -1
  39. package/src/ui/screens/McpScreen.tsx +2 -1
  40. package/src/ui/screens/ModelSelectorScreen.js +2 -2
  41. package/src/ui/screens/ModelSelectorScreen.tsx +3 -2
  42. package/src/ui/screens/ProfilesScreen.js +1 -1
  43. package/src/ui/screens/ProfilesScreen.tsx +2 -1
  44. package/src/ui/screens/StatusLineScreen.js +1 -1
  45. package/src/ui/screens/StatusLineScreen.tsx +2 -1
  46. package/src/ui/state/DimensionsContext.js +2 -2
  47. package/src/ui/state/DimensionsContext.tsx +3 -3
  48. package/src/ui/components/ScrollableDetail.js +0 -23
  49. package/src/ui/components/ScrollableDetail.tsx +0 -55
@@ -3,19 +3,20 @@ import { useApp, useModal } from "../state/AppContext.js";
3
3
  import { useDimensions } from "../state/DimensionsContext.js";
4
4
  import { useKeyboard } from "../hooks/useKeyboard.js";
5
5
  import { ScreenLayout } from "../components/layout/index.js";
6
- import { ScrollableList } from "../components/ScrollableList.js";
7
6
  import {
8
7
  SETTINGS_CATALOG,
9
8
  } from "../../data/settings-catalog.js";
10
9
  import {
11
10
  readAllSettingsBothScopes,
12
11
  writeSettingValue,
12
+ discoverOutputStyles,
13
13
  } from "../../services/settings-manager.js";
14
14
  import {
15
15
  buildSettingsBrowserItems,
16
16
  type SettingsBrowserItem,
17
17
  } from "../adapters/settingsAdapter.js";
18
18
  import { renderSettingRow, renderSettingDetail } from "../renderers/settingsRenderers.js";
19
+ import { ScrollableList } from "../components/ScrollableList.js";
19
20
 
20
21
  export function SettingsScreen() {
21
22
  const { state, dispatch } = useApp();
@@ -26,6 +27,12 @@ export function SettingsScreen() {
26
27
  const fetchData = useCallback(async () => {
27
28
  dispatch({ type: "SETTINGS_DATA_LOADING" });
28
29
  try {
30
+ // Populate dynamic output style options from installed plugins
31
+ const outputStyleSetting = SETTINGS_CATALOG.find((s) => s.id === "output-style");
32
+ if (outputStyleSetting && outputStyleSetting.type === "select") {
33
+ outputStyleSetting.options = await discoverOutputStyles(state.projectPath);
34
+ }
35
+
29
36
  const values = await readAllSettingsBothScopes(
30
37
  SETTINGS_CATALOG,
31
38
  state.projectPath,
@@ -95,22 +102,32 @@ export function SettingsScreen() {
95
102
  await modal.message("Error", `Failed to update: ${error}`, "error");
96
103
  }
97
104
  } else {
98
- const newValue = await modal.input(
99
- `${setting.name} ${scope}`,
100
- setting.description,
101
- currentValue || "",
102
- );
103
- if (newValue === null) return;
104
- try {
105
- await writeSettingValue(
106
- setting,
107
- newValue || undefined,
108
- scope,
109
- state.projectPath,
105
+ // String type: if already set, clear it; if unset, show input modal
106
+ if (currentValue !== undefined && currentValue !== "") {
107
+ try {
108
+ await writeSettingValue(setting, undefined, scope, state.projectPath);
109
+ await fetchData();
110
+ } catch (error) {
111
+ await modal.message("Error", `Failed to update: ${error}`, "error");
112
+ }
113
+ } else {
114
+ const newValue = await modal.input(
115
+ `${setting.name} — ${scope}`,
116
+ setting.description,
117
+ currentValue || setting.defaultValue || "",
110
118
  );
111
- await fetchData();
112
- } catch (error) {
113
- await modal.message("Error", `Failed to update: ${error}`, "error");
119
+ if (newValue === null) return;
120
+ try {
121
+ await writeSettingValue(
122
+ setting,
123
+ newValue || undefined,
124
+ scope,
125
+ state.projectPath,
126
+ );
127
+ await fetchData();
128
+ } catch (error) {
129
+ await modal.message("Error", `Failed to update: ${error}`, "error");
130
+ }
114
131
  }
115
132
  }
116
133
  };
@@ -4,9 +4,9 @@ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
4
4
  import { useDimensions } from "../state/DimensionsContext.js";
5
5
  import { useKeyboard } from "../hooks/useKeyboard.js";
6
6
  import { ScreenLayout } from "../components/layout/index.js";
7
- import { ScrollableList } from "../components/ScrollableList.js";
8
7
  import { searchMcpServers, formatDate } from "../../services/mcp-registry.js";
9
8
  import { addMcpServer, setAllowMcp } from "../../services/claude-settings.js";
9
+ import { ScrollableList } from "../components/ScrollableList.js";
10
10
  /**
11
11
  * Deduplicate servers by name, keeping only the latest version.
12
12
  * Uses version string comparison, falling back to published_at date.
@@ -222,6 +222,6 @@ export function McpRegistryScreen() {
222
222
  isActive: isSearchActive,
223
223
  query: searchQuery,
224
224
  placeholder: searchPlaceholder,
225
- }, footerHints: footerHints, listPanel: isLoading ? (_jsx("text", { fg: "gray", children: "Loading..." })) : error ? (_jsxs("text", { fg: "red", children: ["Error: ", error] })) : servers.length === 0 ? (_jsx("text", { fg: "gray", children: "No servers found" })) : (_jsx(ScrollableList, { items: servers, selectedIndex: mcpRegistry.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight })), detailPanel: renderDetail() }));
225
+ }, footerHints: footerHints, listPanel: isLoading ? (_jsx("text", { fg: "gray", children: "Loading..." })) : error ? (_jsxs("text", { fg: "red", children: ["Error: ", error] })) : servers.length === 0 ? (_jsx("text", { fg: "gray", children: "No servers found" })) : (_jsx(ScrollableList, { items: servers, selectedIndex: mcpRegistry.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight, getKey: (server) => server.name })), detailPanel: renderDetail() }));
226
226
  }
227
227
  export default McpRegistryScreen;
@@ -3,10 +3,11 @@ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
3
3
  import { useDimensions } from "../state/DimensionsContext.js";
4
4
  import { useKeyboard } from "../hooks/useKeyboard.js";
5
5
  import { ScreenLayout } from "../components/layout/index.js";
6
- import { ScrollableList } from "../components/ScrollableList.js";
6
+
7
7
  import { searchMcpServers, formatDate } from "../../services/mcp-registry.js";
8
8
  import { addMcpServer, setAllowMcp } from "../../services/claude-settings.js";
9
9
  import type { McpRegistryServer, McpServerConfig } from "../../types/index.js";
10
+ import { ScrollableList } from "../components/ScrollableList.js";
10
11
 
11
12
  /**
12
13
  * Deduplicate servers by name, keeping only the latest version.
@@ -332,6 +333,7 @@ export function McpRegistryScreen() {
332
333
  selectedIndex={mcpRegistry.selectedIndex}
333
334
  renderItem={renderListItem}
334
335
  maxHeight={dimensions.listPanelHeight}
336
+ getKey={(server) => server.name}
335
337
  />
336
338
  )
337
339
  }
@@ -4,10 +4,10 @@ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
4
4
  import { useDimensions } from "../state/DimensionsContext.js";
5
5
  import { useKeyboard } from "../hooks/useKeyboard.js";
6
6
  import { ScreenLayout } from "../components/layout/index.js";
7
- import { ScrollableList } from "../components/ScrollableList.js";
8
7
  import { getMcpServersByCategory, getCategoryDisplayName, categoryOrder, } from "../../data/mcp-servers.js";
9
8
  import { addMcpServer, removeMcpServer, getInstalledMcpServers, getEnabledMcpServers, } from "../../services/claude-settings.js";
10
9
  import { renderMcpRow, renderMcpDetail, } from "../renderers/mcpRenderers.js";
10
+ import { ScrollableList } from "../components/ScrollableList.js";
11
11
  export function McpScreen() {
12
12
  const { state, dispatch } = useApp();
13
13
  const { mcp } = state;
@@ -3,7 +3,7 @@ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
3
3
  import { useDimensions } from "../state/DimensionsContext.js";
4
4
  import { useKeyboard } from "../hooks/useKeyboard.js";
5
5
  import { ScreenLayout } from "../components/layout/index.js";
6
- import { ScrollableList } from "../components/ScrollableList.js";
6
+
7
7
  import {
8
8
  getMcpServersByCategory,
9
9
  getCategoryDisplayName,
@@ -21,6 +21,7 @@ import {
21
21
  renderMcpDetail,
22
22
  type McpListItem,
23
23
  } from "../renderers/mcpRenderers.js";
24
+ import { ScrollableList } from "../components/ScrollableList.js";
24
25
 
25
26
  export function McpScreen() {
26
27
  const { state, dispatch } = useApp();
@@ -2,9 +2,9 @@ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
2
  import { useMemo, useEffect } from "react";
3
3
  import { useApp } from "../state/AppContext.js";
4
4
  import { useDimensions } from "../state/DimensionsContext.js";
5
- import { ScrollableList } from "../components/ScrollableList.js";
6
5
  import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
7
6
  import { useKeyboard } from "../hooks/useKeyboard.js";
7
+ import { ScrollableList } from "../components/ScrollableList.js";
8
8
  const RECENT_MODELS = [
9
9
  {
10
10
  id: "recent-1",
@@ -285,6 +285,6 @@ export function ModelSelectorScreen() {
285
285
  // Cursor line: > c
286
286
  // Separator: handled by list? or explicit?
287
287
  const listHeight = Math.max(5, dimensions.contentHeight - 5);
288
- return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { flexDirection: "row", border: true, borderStyle: "single", borderColor: "#7e57c2", paddingLeft: 1, paddingRight: 1, children: _jsxs("box", { flexDirection: "column", flexGrow: 1, children: [_jsx("box", { flexDirection: "row", justifyContent: "space-between", children: _jsxs("box", { children: [_jsx("text", { fg: "#7e57c2", children: "Switch Model " }), _jsxs("text", { fg: modelSelector.taskSize === "large" ? "white" : "gray", children: [modelSelector.taskSize === "large" ? "◎" : "○", " Large Task", " "] }), _jsxs("text", { fg: modelSelector.taskSize === "small" ? "white" : "gray", children: [modelSelector.taskSize === "small" ? "◎" : "○", " Small Task"] })] }) }), _jsxs("box", { flexDirection: "row", marginTop: 1, children: [_jsx("text", { fg: "green", children: "> " }), _jsx("text", { children: modelSelector.searchQuery }), _jsx("text", { bg: "gray", fg: "black", children: " " })] })] }) }), _jsx("box", { flexGrow: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(ScrollableList, { items: filteredItems, selectedIndex: modelSelector.selectedIndex, renderItem: renderItem, maxHeight: listHeight, showScrollIndicators: false }) }), _jsx("box", { height: 1, children: _jsx("text", { fg: "#888888", children: footerHints }) })] }));
288
+ return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { flexDirection: "row", border: true, borderStyle: "single", borderColor: "#7e57c2", paddingLeft: 1, paddingRight: 1, children: _jsxs("box", { flexDirection: "column", flexGrow: 1, children: [_jsx("box", { flexDirection: "row", justifyContent: "space-between", children: _jsxs("box", { children: [_jsx("text", { fg: "#7e57c2", children: "Switch Model " }), _jsxs("text", { fg: modelSelector.taskSize === "large" ? "white" : "gray", children: [modelSelector.taskSize === "large" ? "◎" : "○", " Large Task", " "] }), _jsxs("text", { fg: modelSelector.taskSize === "small" ? "white" : "gray", children: [modelSelector.taskSize === "small" ? "◎" : "○", " Small Task"] })] }) }), _jsxs("box", { flexDirection: "row", marginTop: 1, children: [_jsx("text", { fg: "green", children: "> " }), _jsx("text", { children: modelSelector.searchQuery }), _jsx("text", { bg: "gray", fg: "black", children: " " })] })] }) }), _jsx("box", { flexGrow: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(ScrollableList, { items: filteredItems, selectedIndex: modelSelector.selectedIndex, renderItem: renderItem, maxHeight: listHeight, getKey: (item) => item.id }) }), _jsx("box", { height: 1, children: _jsx("text", { fg: "#888888", children: footerHints }) })] }));
289
289
  }
290
290
  export default ModelSelectorScreen;
@@ -1,9 +1,10 @@
1
1
  import React, { useMemo, useEffect } from "react";
2
2
  import { useApp } from "../state/AppContext.js";
3
3
  import { useDimensions } from "../state/DimensionsContext.js";
4
- import { ScrollableList } from "../components/ScrollableList.js";
4
+
5
5
  import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
6
6
  import { useKeyboard } from "../hooks/useKeyboard.js";
7
+ import { ScrollableList } from "../components/ScrollableList.js";
7
8
 
8
9
  interface ModelItem {
9
10
  id: string;
@@ -423,7 +424,7 @@ export function ModelSelectorScreen() {
423
424
  selectedIndex={modelSelector.selectedIndex}
424
425
  renderItem={renderItem}
425
426
  maxHeight={listHeight}
426
- showScrollIndicators={false}
427
+ getKey={(item) => item.id}
427
428
  />
428
429
  </box>
429
430
 
@@ -4,12 +4,12 @@ import { useApp, useModal } from "../state/AppContext.js";
4
4
  import { useDimensions } from "../state/DimensionsContext.js";
5
5
  import { useKeyboard } from "../hooks/useKeyboard.js";
6
6
  import { ScreenLayout } from "../components/layout/index.js";
7
- import { ScrollableList } from "../components/ScrollableList.js";
8
7
  import { listProfiles, applyProfile, renameProfile, deleteProfile, exportProfileToJson, importProfileFromJson, } from "../../services/profiles.js";
9
8
  import { readSettings, writeSettings, } from "../../services/claude-settings.js";
10
9
  import { writeClipboard, readClipboard, ClipboardUnavailableError, } from "../../utils/clipboard.js";
11
10
  import { PREDEFINED_PROFILES, } from "../../data/predefined-profiles.js";
12
11
  import { buildProfileListItems, renderProfileRow, renderProfileDetail, } from "../renderers/profileRenderers.js";
12
+ import { ScrollableList } from "../components/ScrollableList.js";
13
13
  export function ProfilesScreen() {
14
14
  const { state, dispatch } = useApp();
15
15
  const { profiles: profilesState } = state;
@@ -3,7 +3,7 @@ import { useApp, useModal } from "../state/AppContext.js";
3
3
  import { useDimensions } from "../state/DimensionsContext.js";
4
4
  import { useKeyboard } from "../hooks/useKeyboard.js";
5
5
  import { ScreenLayout } from "../components/layout/index.js";
6
- import { ScrollableList } from "../components/ScrollableList.js";
6
+
7
7
  import {
8
8
  listProfiles,
9
9
  applyProfile,
@@ -31,6 +31,7 @@ import {
31
31
  renderProfileDetail,
32
32
  type ProfileListItem,
33
33
  } from "../renderers/profileRenderers.js";
34
+ import { ScrollableList } from "../components/ScrollableList.js";
34
35
 
35
36
  export function ProfilesScreen() {
36
37
  const { state, dispatch } = useApp();
@@ -4,8 +4,8 @@ import { useApp, useModal } from "../state/AppContext.js";
4
4
  import { useDimensions } from "../state/DimensionsContext.js";
5
5
  import { useKeyboard } from "../hooks/useKeyboard.js";
6
6
  import { ScreenLayout } from "../components/layout/index.js";
7
- import { ScrollableList } from "../components/ScrollableList.js";
8
7
  import { statusLineCategories } from "../../data/statuslines.js";
8
+ import { ScrollableList } from "../components/ScrollableList.js";
9
9
  import { setStatusLine, getStatusLine, setGlobalStatusLine, getGlobalStatusLine, } from "../../services/claude-settings.js";
10
10
  export function StatusLineScreen() {
11
11
  const { state, dispatch } = useApp();
@@ -3,9 +3,10 @@ import { useApp, useModal } from "../state/AppContext.js";
3
3
  import { useDimensions } from "../state/DimensionsContext.js";
4
4
  import { useKeyboard } from "../hooks/useKeyboard.js";
5
5
  import { ScreenLayout } from "../components/layout/index.js";
6
- import { ScrollableList } from "../components/ScrollableList.js";
6
+
7
7
  import { statusLineCategories } from "../../data/statuslines.js";
8
8
  import type { StatusLineConfig } from "../../types/index.js";
9
+ import { ScrollableList } from "../components/ScrollableList.js";
9
10
  import {
10
11
  setStatusLine,
11
12
  getStatusLine,
@@ -21,9 +21,9 @@ function calculateDimensions(columns, rows, showProgress, showDebug, showUpdateB
21
21
  contentHeight = Math.max(10, contentHeight); // Minimum 10 lines for full layout
22
22
  // Calculate available content width (accounting for padding)
23
23
  const contentWidth = Math.max(40, terminalWidth - 4);
24
- // Calculate list panel height for ScrollableList
24
+ // Calculate list panel height for scrollbox
25
25
  // ScreenLayout uses: panelHeight = contentHeight - 4 (header) - 1 (footer)
26
- // The ScrollableList sits inside the panel
26
+ // The scrollbox sits inside the panel
27
27
  const listPanelHeight = Math.max(3, contentHeight - SCREEN_HEADER_HEIGHT - SCREEN_FOOTER_HEIGHT);
28
28
  return {
29
29
  terminalWidth,
@@ -10,7 +10,7 @@ interface Dimensions {
10
10
  contentHeight: number;
11
11
  /** Available width for content (excluding borders, padding) */
12
12
  contentWidth: number;
13
- /** Available lines for ScrollableList in list panels */
13
+ /** Available lines for scrollbox in list panels */
14
14
  listPanelHeight: number;
15
15
  }
16
16
 
@@ -52,9 +52,9 @@ function calculateDimensions(
52
52
  // Calculate available content width (accounting for padding)
53
53
  const contentWidth = Math.max(40, terminalWidth - 4);
54
54
 
55
- // Calculate list panel height for ScrollableList
55
+ // Calculate list panel height for scrollbox
56
56
  // ScreenLayout uses: panelHeight = contentHeight - 4 (header) - 1 (footer)
57
- // The ScrollableList sits inside the panel
57
+ // The scrollbox sits inside the panel
58
58
  const listPanelHeight = Math.max(
59
59
  3,
60
60
  contentHeight - SCREEN_HEADER_HEIGHT - SCREEN_FOOTER_HEIGHT,
@@ -1,23 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { useState, useEffect } from "react";
3
- /**
4
- * A scrollable detail panel that renders an array of lines
5
- * with automatic scroll tracking. When content exceeds maxHeight,
6
- * it shows a scroll indicator and clips to fit.
7
- */
8
- export function ScrollableDetail({ lines, maxHeight, scrollTrigger = 0, }) {
9
- const [scrollOffset, setScrollOffset] = useState(0);
10
- // Reset scroll when content changes (new item selected)
11
- useEffect(() => {
12
- setScrollOffset(0);
13
- }, [scrollTrigger]);
14
- const totalLines = lines.length;
15
- const visibleLines = Math.max(1, maxHeight - 1); // -1 for scroll indicator
16
- const canScroll = totalLines > visibleLines;
17
- const maxOffset = Math.max(0, totalLines - visibleLines);
18
- const clampedOffset = Math.min(scrollOffset, maxOffset);
19
- const visibleContent = lines.slice(clampedOffset, clampedOffset + visibleLines);
20
- const scrollUp = clampedOffset > 0;
21
- const scrollDown = clampedOffset < maxOffset;
22
- return (_jsxs("box", { flexDirection: "column", children: [scrollUp && (_jsx("box", { children: _jsxs("text", { fg: "cyan", children: ["\u2191 ", clampedOffset, " more"] }) })), visibleContent, scrollDown && (_jsx("box", { children: _jsxs("text", { fg: "cyan", children: ["\u2193 ", totalLines - clampedOffset - visibleLines, " more"] }) }))] }));
23
- }
@@ -1,55 +0,0 @@
1
- import React, { useState, useEffect } from "react";
2
-
3
- interface ScrollableDetailProps {
4
- /** Array of content lines to display */
5
- lines: React.ReactNode[];
6
- /** Maximum visible height */
7
- maxHeight: number;
8
- /** External scroll trigger — changes when list selection changes */
9
- scrollTrigger?: number;
10
- }
11
-
12
- /**
13
- * A scrollable detail panel that renders an array of lines
14
- * with automatic scroll tracking. When content exceeds maxHeight,
15
- * it shows a scroll indicator and clips to fit.
16
- */
17
- export function ScrollableDetail({
18
- lines,
19
- maxHeight,
20
- scrollTrigger = 0,
21
- }: ScrollableDetailProps) {
22
- const [scrollOffset, setScrollOffset] = useState(0);
23
-
24
- // Reset scroll when content changes (new item selected)
25
- useEffect(() => {
26
- setScrollOffset(0);
27
- }, [scrollTrigger]);
28
-
29
- const totalLines = lines.length;
30
- const visibleLines = Math.max(1, maxHeight - 1); // -1 for scroll indicator
31
- const canScroll = totalLines > visibleLines;
32
- const maxOffset = Math.max(0, totalLines - visibleLines);
33
- const clampedOffset = Math.min(scrollOffset, maxOffset);
34
-
35
- const visibleContent = lines.slice(clampedOffset, clampedOffset + visibleLines);
36
-
37
- const scrollUp = clampedOffset > 0;
38
- const scrollDown = clampedOffset < maxOffset;
39
-
40
- return (
41
- <box flexDirection="column">
42
- {scrollUp && (
43
- <box>
44
- <text fg="cyan">↑ {clampedOffset} more</text>
45
- </box>
46
- )}
47
- {visibleContent}
48
- {scrollDown && (
49
- <box>
50
- <text fg="cyan">↓ {totalLines - clampedOffset - visibleLines} more</text>
51
- </box>
52
- )}
53
- </box>
54
- );
55
- }