rl-rockcli 0.0.8 → 0.0.10

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 (162) hide show
  1. package/commands/attach/basic-repl.js +212 -0
  2. package/commands/attach/cleanup-history.js +189 -0
  3. package/commands/attach/cleanup-manager.js +163 -0
  4. package/commands/attach/copy-ui/copyRepl.js +195 -0
  5. package/commands/attach/copy-ui/index.js +7 -0
  6. package/commands/attach/copy-ui/render/outputBlock.js +25 -0
  7. package/commands/attach/copy-ui/viewport/viewport.js +23 -0
  8. package/commands/attach/copy-ui/viewport/wheel.js +14 -0
  9. package/commands/attach/history-manager.js +507 -0
  10. package/commands/attach/history-session.js +48 -0
  11. package/commands/attach/ink-repl/InkREPL.js +1507 -0
  12. package/commands/attach/ink-repl/builtinCommands.js +1253 -0
  13. package/commands/attach/ink-repl/components/ConnectingScreen.js +76 -0
  14. package/commands/attach/ink-repl/components/Console.js +191 -0
  15. package/commands/attach/ink-repl/components/DetailView.js +148 -0
  16. package/commands/attach/ink-repl/components/DropdownMenu.js +86 -0
  17. package/commands/attach/ink-repl/components/InputArea.js +125 -0
  18. package/commands/attach/ink-repl/components/InputLine.js +18 -0
  19. package/commands/attach/ink-repl/components/OutputArea.js +22 -0
  20. package/commands/attach/ink-repl/components/OutputItem.js +96 -0
  21. package/commands/attach/ink-repl/components/ShellLayout.js +61 -0
  22. package/commands/attach/ink-repl/components/Spinner.js +79 -0
  23. package/commands/attach/ink-repl/components/StatusBar.js +106 -0
  24. package/commands/attach/ink-repl/components/WelcomeBanner.js +48 -0
  25. package/commands/attach/ink-repl/contexts/LayoutContext.js +12 -0
  26. package/commands/attach/ink-repl/contexts/ThemeContext.js +43 -0
  27. package/commands/attach/ink-repl/hooks/useFunctionKeys.js +70 -0
  28. package/commands/attach/ink-repl/hooks/useMouse.js +162 -0
  29. package/commands/attach/ink-repl/hooks/useResources.js +132 -0
  30. package/commands/attach/ink-repl/hooks/useSpinner.js +49 -0
  31. package/commands/attach/ink-repl/index.js +112 -0
  32. package/commands/attach/ink-repl/package.json +3 -0
  33. package/commands/attach/ink-repl/replState.js +947 -0
  34. package/commands/attach/ink-repl/shortcuts/defaultKeybindings.js +138 -0
  35. package/commands/attach/ink-repl/shortcuts/index.js +332 -0
  36. package/commands/attach/ink-repl/themes/defaultDark.js +18 -0
  37. package/commands/attach/ink-repl/themes/defaultLight.js +18 -0
  38. package/commands/attach/ink-repl/themes/index.js +4 -0
  39. package/commands/attach/ink-repl/themes/themeManager.js +45 -0
  40. package/commands/attach/ink-repl/themes/themeTokens.js +15 -0
  41. package/commands/attach/ink-repl/utils/atCompletion.js +346 -0
  42. package/commands/attach/ink-repl/utils/clipboard.js +50 -0
  43. package/commands/attach/ink-repl/utils/consoleLogger.js +81 -0
  44. package/commands/attach/ink-repl/utils/exitCodeHandler.js +49 -0
  45. package/commands/attach/ink-repl/utils/exitCodeTips.js +56 -0
  46. package/commands/attach/ink-repl/utils/formatTime.js +12 -0
  47. package/commands/attach/ink-repl/utils/outputSelection.js +120 -0
  48. package/commands/attach/ink-repl/utils/outputViewport.js +77 -0
  49. package/commands/attach/ink-repl/utils/paginatedFileLoading.js +76 -0
  50. package/commands/attach/ink-repl/utils/paramHint.js +60 -0
  51. package/commands/attach/ink-repl/utils/parseError.js +174 -0
  52. package/commands/attach/ink-repl/utils/pathCompletion.js +167 -0
  53. package/commands/attach/ink-repl/utils/remotePathSafety.js +56 -0
  54. package/commands/attach/ink-repl/utils/replSelection.js +205 -0
  55. package/commands/attach/ink-repl/utils/responseFormatter.js +127 -0
  56. package/commands/attach/ink-repl/utils/textWrap.js +117 -0
  57. package/commands/attach/ink-repl/utils/truncate.js +115 -0
  58. package/commands/attach/opentui-repl/App.tsx +891 -0
  59. package/commands/attach/opentui-repl/builtinCommands.ts +80 -0
  60. package/commands/attach/opentui-repl/components/ConfirmDialog.tsx +116 -0
  61. package/commands/attach/opentui-repl/components/ConnectingScreen.tsx +131 -0
  62. package/commands/attach/opentui-repl/components/Console.tsx +73 -0
  63. package/commands/attach/opentui-repl/components/DetailView.tsx +45 -0
  64. package/commands/attach/opentui-repl/components/DropdownMenu.tsx +130 -0
  65. package/commands/attach/opentui-repl/components/ExecutionStatus.tsx +66 -0
  66. package/commands/attach/opentui-repl/components/Header.tsx +24 -0
  67. package/commands/attach/opentui-repl/components/OutputArea.tsx +25 -0
  68. package/commands/attach/opentui-repl/components/OutputBlock.tsx +108 -0
  69. package/commands/attach/opentui-repl/components/PromptInput.tsx +109 -0
  70. package/commands/attach/opentui-repl/components/StatusBar.tsx +63 -0
  71. package/commands/attach/opentui-repl/components/Toast.tsx +65 -0
  72. package/commands/attach/opentui-repl/components/WelcomeBanner.tsx +41 -0
  73. package/commands/attach/opentui-repl/contexts/ReplContext.tsx +137 -0
  74. package/commands/attach/opentui-repl/contexts/SessionContext.tsx +32 -0
  75. package/commands/attach/opentui-repl/contexts/ThemeContext.tsx +70 -0
  76. package/commands/attach/opentui-repl/contexts/ToastContext.tsx +69 -0
  77. package/commands/attach/opentui-repl/contexts/toast-logic.js +71 -0
  78. package/commands/attach/opentui-repl/hooks/useResources.ts +102 -0
  79. package/commands/attach/opentui-repl/hooks/useSpinner.ts +46 -0
  80. package/commands/attach/opentui-repl/index.js +99 -0
  81. package/commands/attach/opentui-repl/keybindings.ts +39 -0
  82. package/commands/attach/opentui-repl/package.json +3 -0
  83. package/commands/attach/opentui-repl/render.tsx +72 -0
  84. package/commands/attach/opentui-repl/tsconfig.json +12 -0
  85. package/commands/attach/repl.js +791 -0
  86. package/commands/attach/sandbox-id-resolver.js +56 -0
  87. package/commands/attach/session-manager.js +307 -0
  88. package/commands/attach/ui-mode.js +146 -0
  89. package/commands/log/core/constants.js +237 -0
  90. package/commands/log/core/display.js +370 -0
  91. package/commands/log/core/search.js +330 -0
  92. package/commands/log/core/tail.js +216 -0
  93. package/commands/log/core/utils.js +424 -0
  94. package/commands/log.js +298 -0
  95. package/commands/sandbox/core/log-bridge.js +119 -0
  96. package/commands/sandbox/core/replay/analyzer.js +311 -0
  97. package/commands/sandbox/core/replay/batch-orchestrator.js +536 -0
  98. package/commands/sandbox/core/replay/batch-task.js +369 -0
  99. package/commands/sandbox/core/replay/concurrent-display.js +70 -0
  100. package/commands/sandbox/core/replay/concurrent-orchestrator.js +170 -0
  101. package/commands/sandbox/core/replay/data-source.js +86 -0
  102. package/commands/sandbox/core/replay/display.js +231 -0
  103. package/commands/sandbox/core/replay/executor.js +634 -0
  104. package/commands/sandbox/core/replay/history-fetcher.js +124 -0
  105. package/commands/sandbox/core/replay/index.js +338 -0
  106. package/commands/sandbox/core/replay/loghouse-data-source.js +177 -0
  107. package/commands/sandbox/core/replay/pid-mapping.js +26 -0
  108. package/commands/sandbox/core/replay/request.js +109 -0
  109. package/commands/sandbox/core/replay/worker.js +166 -0
  110. package/commands/sandbox/core/session.js +346 -0
  111. package/commands/sandbox/log-bridge.js +2 -0
  112. package/commands/sandbox/ray.js +2 -0
  113. package/commands/sandbox/replay/analyzer.js +311 -0
  114. package/commands/sandbox/replay/batch-orchestrator.js +536 -0
  115. package/commands/sandbox/replay/batch-task.js +369 -0
  116. package/commands/sandbox/replay/concurrent-display.js +70 -0
  117. package/commands/sandbox/replay/concurrent-orchestrator.js +170 -0
  118. package/commands/sandbox/replay/display.js +231 -0
  119. package/commands/sandbox/replay/executor.js +634 -0
  120. package/commands/sandbox/replay/history-fetcher.js +118 -0
  121. package/commands/sandbox/replay/index.js +338 -0
  122. package/commands/sandbox/replay/pid-mapping.js +26 -0
  123. package/commands/sandbox/replay/request.js +109 -0
  124. package/commands/sandbox/replay/worker.js +166 -0
  125. package/commands/sandbox/replay.js +2 -0
  126. package/commands/sandbox/session.js +2 -0
  127. package/commands/sandbox-original.js +1393 -0
  128. package/commands/sandbox.js +499 -0
  129. package/help/help.json +1071 -0
  130. package/help/middleware.js +71 -0
  131. package/help/renderer.js +800 -0
  132. package/index.js +5 -15
  133. package/lib/plugin-context.js +40 -0
  134. package/package.json +2 -2
  135. package/sdks/sandbox/core/client.js +845 -0
  136. package/sdks/sandbox/core/config.js +70 -0
  137. package/sdks/sandbox/core/types.js +74 -0
  138. package/sdks/sandbox/httpLogger.js +251 -0
  139. package/sdks/sandbox/index.js +9 -0
  140. package/utils/asciiArt.js +138 -0
  141. package/utils/bun-compat.js +59 -0
  142. package/utils/ciPipelines.js +138 -0
  143. package/utils/cli.js +17 -0
  144. package/utils/command-router.js +79 -0
  145. package/utils/configManager.js +503 -0
  146. package/utils/dependency-resolver.js +135 -0
  147. package/utils/eagleeye_traceid.js +151 -0
  148. package/utils/envDetector.js +78 -0
  149. package/utils/execution_logger.js +415 -0
  150. package/utils/featureManager.js +68 -0
  151. package/utils/firstTimeTip.js +44 -0
  152. package/utils/hook-manager.js +125 -0
  153. package/utils/http-logger.js +264 -0
  154. package/utils/i18n.js +139 -0
  155. package/utils/image-progress.js +159 -0
  156. package/utils/logger.js +154 -0
  157. package/utils/plugin-loader.js +124 -0
  158. package/utils/plugin-manager.js +348 -0
  159. package/utils/ray_cli_wrapper.js +746 -0
  160. package/utils/sandbox-client.js +419 -0
  161. package/utils/terminal.js +32 -0
  162. package/utils/tips.js +106 -0
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { useTheme } from '../contexts/ThemeContext.js';
4
+ import { useSpinner } from '../hooks/useSpinner.js';
5
+ import i18n from '../../../../utils/i18n.js';
6
+ const { t } = i18n;
7
+
8
+ const h = React.createElement;
9
+
10
+ const ASCII_LINES = [
11
+ '██████╗ ██████ ██████╗ ██╗ ██╗ ██████╗ ██╗ ██╗',
12
+ '██╔══██╗ ██╔═══██╗ ██╔════╝ ██║ ██╔╝ ██╔════╝ ██║ ██║',
13
+ '██████╔╝ ██║ ██║ ██║ █████╔╝ ██║ ██║ ██║',
14
+ '██╔══██╗ ██║ ██║ ██║ ██╔═██╗ ██║ ██║ ██║',
15
+ '██║ ██║ ╚██████╔╝ ╚██████╗ ██║ ██╗ ╚██████╗ ███████╗ ██║',
16
+ '╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝',
17
+ ];
18
+
19
+ /**
20
+ * Connecting screen with ROCK CLI branding
21
+ * @param {Object} props
22
+ * @param {string} props.sandboxId - Sandbox ID being connected to
23
+ * @param {number} props.attempt - Current attempt number
24
+ * @param {number} props.maxAttempts - Maximum number of attempts
25
+ */
26
+ export function ConnectingScreen({ sandboxId, attempt = 1, maxAttempts = 30 }) {
27
+ const { theme } = useTheme();
28
+ const { frame } = useSpinner(true);
29
+
30
+ let gradient = theme.colors.gradient || ['#89b4fa', '#cba6f7', '#f38ba8'];
31
+ if (theme.semantic && theme.semantic.banner) {
32
+ gradient = theme.semantic.banner;
33
+ }
34
+
35
+ const connectingText = t('connecting.message') || '🔄 正在连接沙箱...';
36
+ const attemptText = attempt > 1 ? t('connecting.attempt') || `尝试 ${attempt}/${maxAttempts} 次...` : '';
37
+
38
+ return h(Box, {
39
+ flexDirection: 'column',
40
+ alignItems: 'center',
41
+ justifyContent: 'center',
42
+ paddingTop: 2,
43
+ paddingBottom: 2,
44
+ },
45
+ // ASCII Art Banner
46
+ h(Box, { flexDirection: 'column', alignItems: 'center', marginBottom: 2 },
47
+ ASCII_LINES.map((line, index) =>
48
+ h(Text, {
49
+ key: `banner-line-${index}`,
50
+ color: gradient[index % gradient.length],
51
+ bold: true,
52
+ }, line)
53
+ )
54
+ ),
55
+
56
+ // Connecting message
57
+ h(Box, { marginBottom: 1 },
58
+ h(Text, { color: theme.colors.primary || '#89b4fa' }, connectingText)
59
+ ),
60
+
61
+ // Sandbox ID
62
+ h(Box, { marginBottom: 1 },
63
+ h(Text, { color: theme.colors.textSecondary || 'gray' }, sandboxId)
64
+ ),
65
+
66
+ // Spinner
67
+ h(Box, { marginBottom: 1 },
68
+ h(Text, { color: 'yellow' }, `${frame} Loading...`)
69
+ ),
70
+
71
+ // Attempt counter (only show if attempt > 1)
72
+ attempt > 1 ? h(Box, {},
73
+ h(Text, { color: theme.colors.textSecondary || 'gray' }, attemptText.replace('${attempt}', attempt).replace('${maxAttempts}', maxAttempts))
74
+ ) : null
75
+ );
76
+ }
@@ -0,0 +1,191 @@
1
+ import React from 'react';
2
+ import { Box, Text, useStdout } from 'ink';
3
+ import { getLineSelectionRanges } from '../replState.js';
4
+ import { useTheme } from '../contexts/ThemeContext.js';
5
+
6
+ const h = React.createElement;
7
+
8
+ /**
9
+ * Truncate text to fit terminal width
10
+ * @param {string} text Text to truncate
11
+ * @param {number} maxWidth Maximum width
12
+ * @returns {string} Truncated text
13
+ */
14
+ function truncateText(text, maxWidth) {
15
+ if (!text || text.length <= maxWidth) return text;
16
+ return text.slice(0, maxWidth - 3) + '...';
17
+ }
18
+
19
+ /**
20
+ * Render a log line with selection highlighting
21
+ * @param {string} text - The log message
22
+ * @param {number} lineIndex - 0-based line index in visible area
23
+ * @param {number} scrollOffset - Current scroll offset
24
+ * @param {Object} selection - Selection state
25
+ * @param {string} color - Log level color
26
+ * @param {boolean} isBold - Whether to bold text
27
+ * @returns {React.Element}
28
+ */
29
+ function renderLogWithSelection(text, lineIndex, scrollOffset, selection, color, isBold) {
30
+ const actualLineIndex = scrollOffset + lineIndex;
31
+ const maxWidth = text.length;
32
+
33
+ if (!selection || selection.source !== 'console') {
34
+ return h(Text, {
35
+ color,
36
+ bold: isBold,
37
+ wrap: 'truncate',
38
+ }, text);
39
+ }
40
+
41
+ const ranges = getLineSelectionRanges(selection, actualLineIndex, maxWidth);
42
+
43
+ if (ranges.length === 0) {
44
+ return h(Text, {
45
+ color,
46
+ bold: isBold,
47
+ wrap: 'truncate',
48
+ }, text);
49
+ }
50
+
51
+ // Split text into segments based on selection ranges
52
+ const segments = [];
53
+ let lastEnd = 0;
54
+
55
+ for (const range of ranges) {
56
+ // Unselected part before this range
57
+ if (range.start > lastEnd) {
58
+ segments.push(
59
+ h(Text, {
60
+ key: `seg-${lastEnd}-unselected`,
61
+ color,
62
+ bold: isBold,
63
+ }, text.substring(lastEnd, range.start))
64
+ );
65
+ }
66
+
67
+ // Selected part
68
+ segments.push(
69
+ h(Text,
70
+ {
71
+ key: `seg-${range.start}-selected`,
72
+ inverse: true,
73
+ color: 'cyan',
74
+ bold: isBold,
75
+ },
76
+ text.substring(range.start, range.end)
77
+ )
78
+ );
79
+
80
+ lastEnd = range.end;
81
+ }
82
+
83
+ // Unselected part after last range
84
+ if (lastEnd < text.length) {
85
+ segments.push(
86
+ h(Text, {
87
+ key: `seg-${lastEnd}-tail`,
88
+ color,
89
+ bold: isBold,
90
+ }, text.substring(lastEnd))
91
+ );
92
+ }
93
+
94
+ return h(Text, { wrap: 'truncate' }, ...segments);
95
+ }
96
+
97
+ /**
98
+ * Console component - Developer console for debug logs
99
+ * Toggle visibility with F12
100
+ * Use ↑↓ to select, Enter to copy
101
+ * Supports mouse drag selection
102
+ */
103
+ export function Console({
104
+ visible = false,
105
+ logs = [],
106
+ height = 8,
107
+ scrollOffset = 0,
108
+ selectedIndex = -1,
109
+ selection = null,
110
+ }) {
111
+ const { stdout } = useStdout();
112
+ const { theme } = useTheme();
113
+
114
+ if (!visible) return null;
115
+
116
+ // Get terminal width, subtract border (2) and padding (2) and selection indicator (2)
117
+ const terminalWidth = stdout?.columns || 120;
118
+ const contentWidth = terminalWidth - 6;
119
+
120
+ // Calculate visible lines
121
+ const visibleLogs = logs.slice(scrollOffset, scrollOffset + height);
122
+
123
+ // Build hint text based on selection state
124
+ const hasDragSelection = selection && selection.source === 'console' && selection.content;
125
+ const hintText = hasDragSelection
126
+ ? `${logs.length} logs selection active ⏎ copy esc clear`
127
+ : selectedIndex >= 0
128
+ ? `${logs.length} logs ↑↓ select ⏎ copy esc cancel`
129
+ : `${logs.length} logs ↑↓ select F12 close`;
130
+
131
+ return h(Box, {
132
+ flexDirection: 'column',
133
+ borderStyle: 'round',
134
+ borderColor: hasDragSelection ? theme.colors.accent : theme.colors.border,
135
+ paddingLeft: 1,
136
+ paddingRight: 1,
137
+ height: height + 2,
138
+ },
139
+ // Header
140
+ h(Box, { justifyContent: 'flex-start' },
141
+ h(Text, { color: theme.colors.accent, bold: true }, 'CONSOLE'),
142
+ h(Text, { color: theme.colors.textSecondary }, ` ${hintText}`)
143
+ ),
144
+ // Log entries with selection support
145
+ ...visibleLogs.map((log, index) => {
146
+ const logIndex = scrollOffset + index;
147
+ const isSelected = logIndex === selectedIndex;
148
+ const displayText = truncateText(log.message, contentWidth);
149
+
150
+ return h(Box, { key: `log-${logIndex}` },
151
+ // Selection indicator (keyboard selection)
152
+ h(Text, { color: theme.colors.accent }, isSelected ? '› ' : ' '),
153
+ // Log text with drag selection support
154
+ renderLogWithSelection(
155
+ displayText,
156
+ index,
157
+ scrollOffset,
158
+ selection,
159
+ getLogColor(log.level, theme),
160
+ log.level === 'error'
161
+ )
162
+ );
163
+ }),
164
+ // Empty lines if not enough logs
165
+ ...Array(Math.max(0, height - visibleLogs.length - 1)).fill(null).map((_, i) =>
166
+ h(Box, { key: `empty-${i}` }, h(Text, null, ' '))
167
+ )
168
+ );
169
+ }
170
+
171
+ /**
172
+ * Get color based on log level
173
+ */
174
+ function getLogColor(level, theme) {
175
+ switch (level) {
176
+ case 'error': return theme.colors.danger;
177
+ case 'warn': return theme.colors.warning;
178
+ case 'info': return theme.colors.accent;
179
+ case 'debug': return theme.colors.textSecondary;
180
+ default: return theme.colors.textPrimary;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Format timestamp for display
186
+ */
187
+ function formatTime(date) {
188
+ if (!date) return '--:--:--';
189
+ const d = date instanceof Date ? date : new Date(date);
190
+ return d.toTimeString().slice(0, 8);
191
+ }
@@ -0,0 +1,148 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Box, Text, useStdout } from 'ink';
3
+ import { getLineSelectionRanges } from '../replState.js';
4
+
5
+ const h = React.createElement;
6
+
7
+ /**
8
+ * Render a line with selection highlighting
9
+ * @param {string} line - The text line
10
+ * @param {number} lineIndex - 0-based line index in visible area
11
+ * @param {number} scrollOffset - Current scroll offset
12
+ * @param {Object} selection - Selection state
13
+ * @returns {React.Element}
14
+ */
15
+ function renderLineWithSelection(line, lineIndex, scrollOffset, selection) {
16
+ const actualLineIndex = scrollOffset + lineIndex;
17
+ const lineText = line || ' ';
18
+
19
+ if (!selection) {
20
+ return h(Text, { key: `line-${lineIndex}` }, lineText);
21
+ }
22
+
23
+ const ranges = getLineSelectionRanges(selection, actualLineIndex, lineText.length);
24
+
25
+ if (ranges.length === 0) {
26
+ return h(Text, { key: `line-${lineIndex}` }, lineText);
27
+ }
28
+
29
+ // Split line into segments based on selection ranges
30
+ const segments = [];
31
+ let lastEnd = 0;
32
+
33
+ for (const range of ranges) {
34
+ // Unselected part before this range
35
+ if (range.start > lastEnd) {
36
+ segments.push(
37
+ h(Text, { key: `seg-${lastEnd}-unselected` },
38
+ lineText.substring(lastEnd, range.start)
39
+ )
40
+ );
41
+ }
42
+
43
+ // Selected part
44
+ segments.push(
45
+ h(Text,
46
+ {
47
+ key: `seg-${range.start}-selected`,
48
+ inverse: true,
49
+ color: 'cyan',
50
+ },
51
+ lineText.substring(range.start, range.end)
52
+ )
53
+ );
54
+
55
+ lastEnd = range.end;
56
+ }
57
+
58
+ // Unselected part after last range
59
+ if (lastEnd < lineText.length) {
60
+ segments.push(
61
+ h(Text, { key: `seg-${lastEnd}-tail` },
62
+ lineText.substring(lastEnd)
63
+ )
64
+ );
65
+ }
66
+
67
+ return h(Text, { key: `line-${lineIndex}` }, ...segments);
68
+ }
69
+
70
+ /**
71
+ * Detail view for full output
72
+ * @param {string} title - Command title
73
+ * @param {string} content - Full output content
74
+ * @param {string[]} lines - Pre-wrapped content lines (optional)
75
+ * @param {number} scrollOffset - Current scroll position
76
+ * @param {number} availableHeight - Available height passed from parent (excludes status bar)
77
+ * @param {Object} selection - Selection state for highlighting
78
+ * @param {Object} paginatedFile - Paginated file state (if applicable)
79
+ */
80
+ export function DetailView({ title, content, lines = null, scrollOffset = 0, availableHeight, selection = null, paginatedFile = null }) {
81
+ const { stdout } = useStdout();
82
+ const width = stdout ? stdout.columns || 80 : 80;
83
+ const terminalHeight = stdout ? stdout.rows || 24 : 24;
84
+
85
+ // Use passed height or calculate from terminal (minus status bar)
86
+ const totalHeight = availableHeight || (terminalHeight - 1);
87
+
88
+ // Fixed layout: header (2) + content area (rest)
89
+ const headerHeight = 2;
90
+
91
+ // Use wrapped lines if provided; otherwise fallback to logical lines.
92
+ const allLines = useMemo(() => {
93
+ if (Array.isArray(lines) && lines.length >= 0) return lines;
94
+ return String(content ?? '').split('\n');
95
+ }, [content, lines]);
96
+
97
+ // Fixed content height (total - header)
98
+ const contentHeight = totalHeight - headerHeight;
99
+
100
+ // Check if scrolling is needed
101
+ const needsScrolling = allLines.length > contentHeight;
102
+
103
+ // Get visible lines - exactly contentHeight lines
104
+ const visibleLines = allLines.slice(scrollOffset, scrollOffset + contentHeight);
105
+
106
+ // Build scroll info string (shown in header area)
107
+ let scrollInfo = '';
108
+ if (needsScrolling) {
109
+ const endLine = Math.min(scrollOffset + contentHeight, allLines.length);
110
+ scrollInfo = ` [${scrollOffset + 1}-${endLine}/${allLines.length}]`;
111
+ }
112
+
113
+ // Add loading indicator if paginated file is loading
114
+ let loadingIndicator = '';
115
+ if (paginatedFile && paginatedFile.isLoading) {
116
+ loadingIndicator = ' ⟳ Loading...';
117
+ }
118
+
119
+ // Build all output lines (exactly contentHeight lines)
120
+ // This ensures stable rendering without relying on Ink's flexbox
121
+ const outputLines = [];
122
+ for (let i = 0; i < contentHeight; i++) {
123
+ if (i < visibleLines.length) {
124
+ outputLines.push(visibleLines[i]);
125
+ } else {
126
+ outputLines.push(''); // Empty padding line
127
+ }
128
+ }
129
+
130
+ return h(Box, { flexDirection: 'column' },
131
+ // Fixed Header with scroll info
132
+ h(Text, null,
133
+ h(Text, { color: 'cyan' }, '← back (esc)'),
134
+ h(Text, { dimColor: true }, ' │ '),
135
+ h(Text, { color: 'green' }, 'Ctrl+Y copy'),
136
+ h(Text, { dimColor: true }, scrollInfo),
137
+ h(Text, { color: 'yellow' }, loadingIndicator),
138
+ h(Text, null, ' '.repeat(Math.max(1, width - 12 - 11 - scrollInfo.length - loadingIndicator.length - title.length - 10))),
139
+ h(Text, { dimColor: true }, 'Output: ', title)
140
+ ),
141
+ h(Text, { dimColor: true }, '─'.repeat(width)),
142
+
143
+ // Content area - render exactly contentHeight lines with selection support
144
+ ...outputLines.map((line, i) =>
145
+ renderLineWithSelection(line, i, scrollOffset, selection)
146
+ )
147
+ );
148
+ }
@@ -0,0 +1,86 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Box, Text, useStdout } from 'ink';
3
+
4
+ const h = React.createElement;
5
+
6
+ /**
7
+ * Truncate text with ellipsis
8
+ */
9
+ export function truncateText(text, maxLength) {
10
+ if (!text || text.length <= maxLength) return text || '';
11
+ return text.slice(0, maxLength - 1) + '…';
12
+ }
13
+
14
+ /**
15
+ * Single menu item - Claude Code style
16
+ * Layout: "› /command description text..."
17
+ */
18
+ export function MenuItem({ item, isSelected, labelWidth, descWidth }) {
19
+ const label = item.label.padEnd(labelWidth);
20
+ const desc = truncateText(item.description, descWidth);
21
+
22
+ // Use › indicator for selected item
23
+ const prefix = isSelected ? '› ' : ' ';
24
+ const labelStyle = isSelected
25
+ ? { color: 'cyan', bold: true }
26
+ : { color: 'white' };
27
+
28
+ return h(Box, null,
29
+ h(Text, { color: isSelected ? 'cyan' : undefined }, prefix),
30
+ h(Text, labelStyle, label),
31
+ h(Text, null, ' '), // 4 spaces gap between command and description
32
+ h(Text, { dimColor: true }, desc)
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Dropdown menu with scrolling - Claude Code style
38
+ */
39
+ export function DropdownMenu({ items, selectedIndex, maxVisible = 8 }) {
40
+ const { stdout } = useStdout();
41
+ const termWidth = stdout ? stdout.columns || 80 : 80;
42
+
43
+ // Calculate scroll offset
44
+ const scrollOffset = useMemo(() => {
45
+ if (items.length <= maxVisible) return 0;
46
+ const halfVisible = Math.floor(maxVisible / 2);
47
+ let offset = selectedIndex - halfVisible;
48
+ offset = Math.max(0, offset);
49
+ offset = Math.min(items.length - maxVisible, offset);
50
+ return offset;
51
+ }, [items.length, selectedIndex, maxVisible]);
52
+
53
+ // Calculate column widths based on longest label
54
+ const labelWidth = useMemo(() => {
55
+ const maxLabel = Math.max(...items.map(i => i.label.length));
56
+ return Math.min(maxLabel + 2, 40); // Cap at 40 chars
57
+ }, [items]);
58
+
59
+ // Description takes remaining space (minus prefix and gaps: 2 + 4 + some buffer)
60
+ const descWidth = Math.max(20, termWidth - labelWidth - 10);
61
+
62
+ const visibleItems = items.slice(scrollOffset, scrollOffset + maxVisible);
63
+ const hasMoreAbove = scrollOffset > 0;
64
+ const hasMoreBelow = scrollOffset + maxVisible < items.length;
65
+
66
+ return h(Box, { flexDirection: 'column' },
67
+ // Scroll up indicator
68
+ hasMoreAbove
69
+ ? h(Text, { dimColor: true }, ' ↑ ', scrollOffset, ' more')
70
+ : null,
71
+ // Menu items
72
+ ...visibleItems.map((item, index) =>
73
+ h(MenuItem, {
74
+ key: item.value || `item-${index}`,
75
+ item: item,
76
+ isSelected: (index + scrollOffset) === selectedIndex,
77
+ labelWidth: labelWidth,
78
+ descWidth: descWidth,
79
+ })
80
+ ),
81
+ // Scroll down indicator
82
+ hasMoreBelow
83
+ ? h(Text, { dimColor: true }, ' ↓ ', items.length - scrollOffset - maxVisible, ' more')
84
+ : null
85
+ );
86
+ }
@@ -0,0 +1,125 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Box, Text, useStdout } from 'ink';
3
+ import { useTheme } from '../contexts/ThemeContext.js';
4
+ import { useLayout } from '../contexts/LayoutContext.js';
5
+ import { getPlaceholderText } from '../utils/paramHint.js';
6
+
7
+ /**
8
+ * Create ANSI background color escape sequence from hex color
9
+ * This avoids issues with the bgHex method being obfuscated incorrectly
10
+ */
11
+ function hexToAnsiBg(hexColor) {
12
+ // Remove # if present
13
+ const hex = hexColor.replace('#', '');
14
+ // Parse RGB values
15
+ const r = parseInt(hex.substring(0, 2), 16);
16
+ const g = parseInt(hex.substring(2, 4), 16);
17
+ const b = parseInt(hex.substring(4, 6), 16);
18
+ // Return ANSI escape sequence for background color
19
+ return `\x1b[48;2;${r};${g};${b}m`;
20
+ }
21
+
22
+ const h = React.createElement;
23
+
24
+ /**
25
+ * Calculate the display width of a string (handling wide characters like CJK)
26
+ */
27
+ function stringWidth(str) {
28
+ let width = 0;
29
+ for (const char of str) {
30
+ const code = char.codePointAt(0);
31
+ // CJK characters and other wide characters
32
+ if (
33
+ (code >= 0x1100 && code <= 0x115f) || // Hangul Jamo
34
+ (code >= 0x2e80 && code <= 0xa4cf) || // CJK
35
+ (code >= 0xac00 && code <= 0xd7a3) || // Hangul Syllables
36
+ (code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs
37
+ (code >= 0xfe10 && code <= 0xfe1f) || // Vertical forms
38
+ (code >= 0xfe30 && code <= 0xfe6f) || // CJK Compatibility Forms
39
+ (code >= 0xff00 && code <= 0xff60) || // Fullwidth Forms
40
+ (code >= 0xffe0 && code <= 0xffe6) || // Fullwidth Forms
41
+ (code >= 0x20000 && code <= 0x2fffd) || // CJK Extension B
42
+ (code >= 0x30000 && code <= 0x3fffd) // CJK Extension C
43
+ ) {
44
+ width += 2;
45
+ } else {
46
+ width += 1;
47
+ }
48
+ }
49
+ return width;
50
+ }
51
+
52
+ /**
53
+ * Input area with separator lines (like Claude Code style)
54
+ *
55
+ * Layout:
56
+ * ────────────────────────────
57
+ * ❯ user input here█ @<param>
58
+ * ────────────────────────────
59
+ *
60
+ * Shows parameter hints (placeholder text) for /upload and /download commands.
61
+ */
62
+ export function InputArea({
63
+ prompt = '❯ ',
64
+ buffer = '',
65
+ cursorPosition = null,
66
+ showCursor = true,
67
+ menuVisible = false,
68
+ menuType = null
69
+ }) {
70
+ const { stdout } = useStdout();
71
+ const { theme } = useTheme();
72
+ const { width: layoutWidth } = useLayout();
73
+ const fallbackWidth = stdout ? stdout.columns || 80 : 80;
74
+ const width = Math.max(1, layoutWidth || fallbackWidth);
75
+
76
+ // Memoize separator to avoid recalculating on every render
77
+ // Use bold line character '━' for thicker appearance
78
+ const separator = useMemo(() => '━'.repeat(width), [width]);
79
+
80
+ // Blue color for input area separator (like Claude Code style)
81
+ const separatorColor = '#4a90d9';
82
+
83
+ // Cursor position defaults to end of buffer if not specified
84
+ const actualCursorPos = cursorPosition !== null && cursorPosition !== undefined ? cursorPosition : buffer.length;
85
+
86
+ // Split buffer at cursor position
87
+ const beforeCursor = buffer.slice(0, actualCursorPos);
88
+ const atCursor = buffer.slice(actualCursorPos, actualCursorPos + 1); // Character at cursor
89
+ const afterCursor = buffer.slice(actualCursorPos + 1);
90
+
91
+ // Calculate placeholder text for parameter hints
92
+ const placeholderText = useMemo(() => {
93
+ // Don't show if menu is visible and it's a slash menu
94
+ if (menuVisible && menuType === 'slash') {
95
+ return null;
96
+ }
97
+ return getPlaceholderText(buffer, actualCursorPos);
98
+ }, [buffer, actualCursorPos, menuVisible, menuType]);
99
+
100
+ return h(Box, { flexDirection: 'column', width, marginTop: 0, flexShrink: 0 },
101
+ // Top separator (bold and dark)
102
+ h(Text, { color: separatorColor, wrap: 'truncate' }, separator),
103
+ // Input line with padding
104
+ h(Box, { paddingLeft: 1, paddingTop: 1, paddingBottom: 1, width: Math.max(1, width - 1) },
105
+ h(Text, { color: theme.colors.prompt }, prompt),
106
+ // Render buffer before cursor
107
+ h(Text, null, beforeCursor),
108
+ // Render cursor block: either covering a character or showing as space at end
109
+ showCursor ? h(Text, null,
110
+ hexToAnsiBg(theme.colors.cursor) + (atCursor || ' ') + '\x1b[0m'
111
+ ) : (
112
+ // When cursor is hidden, still show the character at cursor position
113
+ atCursor ? h(Text, null, atCursor) : null
114
+ ),
115
+ // Render buffer after cursor
116
+ h(Text, null, afterCursor),
117
+ // Render placeholder (FR-005: secondary text color)
118
+ placeholderText ? h(Text, { color: theme.colors.textSecondary },
119
+ ' ' + placeholderText
120
+ ) : null
121
+ ),
122
+ // Bottom separator (bold and dark)
123
+ h(Text, { color: separatorColor, wrap: 'truncate' }, separator)
124
+ );
125
+ }
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const React = require('react');
4
+ const { Box, Text } = require('ink');
5
+ const h = React.createElement;
6
+
7
+ /**
8
+ * Input line with prompt, buffer, and cursor
9
+ */
10
+ function InputLine({ prompt = '', buffer = '', showCursor = true }) {
11
+ return h(Box, null,
12
+ h(Text, { color: 'green' }, prompt),
13
+ h(Text, null, buffer),
14
+ showCursor ? h(Text, { inverse: true }, ' ') : null
15
+ );
16
+ }
17
+
18
+ module.exports = { InputLine };
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { Box, useStdout } from 'ink';
3
+ import { useLayout } from '../contexts/LayoutContext.js';
4
+
5
+ const h = React.createElement;
6
+
7
+ export function OutputArea({ children }) {
8
+ const { stdout } = useStdout();
9
+ const { width: layoutWidth } = useLayout();
10
+ const fallbackWidth = stdout?.columns || 80;
11
+ const width = Math.max(1, layoutWidth || fallbackWidth);
12
+
13
+ return h(Box, {
14
+ flexDirection: 'column',
15
+ width,
16
+ flexGrow: 1,
17
+ flexShrink: 1,
18
+ minHeight: 1,
19
+ rowGap: 0,
20
+ paddingBottom: 0, // Remove padding, handled by RESERVED_FIXED_ROWS
21
+ }, children);
22
+ }