veryfront 0.0.82 → 0.0.83

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 (120) hide show
  1. package/README.md +15 -1
  2. package/esm/deno.js +1 -1
  3. package/esm/proxy/cache/index.d.ts +41 -0
  4. package/esm/proxy/cache/index.d.ts.map +1 -0
  5. package/esm/proxy/cache/index.js +75 -0
  6. package/esm/proxy/cache/memory-cache.d.ts +18 -0
  7. package/esm/proxy/cache/memory-cache.d.ts.map +1 -0
  8. package/esm/proxy/cache/memory-cache.js +100 -0
  9. package/esm/proxy/cache/redis-cache.d.ts +27 -0
  10. package/esm/proxy/cache/redis-cache.d.ts.map +1 -0
  11. package/esm/proxy/cache/redis-cache.js +183 -0
  12. package/esm/proxy/cache/resilient-cache.d.ts +44 -0
  13. package/esm/proxy/cache/resilient-cache.d.ts.map +1 -0
  14. package/esm/proxy/cache/resilient-cache.js +178 -0
  15. package/esm/proxy/cache/types.d.ts +65 -0
  16. package/esm/proxy/cache/types.d.ts.map +1 -0
  17. package/esm/proxy/cache/types.js +7 -0
  18. package/esm/proxy/handler.d.ts +81 -0
  19. package/esm/proxy/handler.d.ts.map +1 -0
  20. package/esm/proxy/handler.js +417 -0
  21. package/esm/proxy/logger.d.ts +29 -0
  22. package/esm/proxy/logger.d.ts.map +1 -0
  23. package/esm/proxy/logger.js +258 -0
  24. package/esm/proxy/oauth-client.d.ts +15 -0
  25. package/esm/proxy/oauth-client.d.ts.map +1 -0
  26. package/esm/proxy/oauth-client.js +52 -0
  27. package/esm/proxy/token-manager.d.ts +59 -0
  28. package/esm/proxy/token-manager.d.ts.map +1 -0
  29. package/esm/proxy/token-manager.js +125 -0
  30. package/esm/proxy/tracing.d.ts +39 -0
  31. package/esm/proxy/tracing.d.ts.map +1 -0
  32. package/esm/proxy/tracing.js +194 -0
  33. package/esm/src/cache/backend.d.ts +2 -0
  34. package/esm/src/cache/backend.d.ts.map +1 -1
  35. package/esm/src/cache/backend.js +2 -0
  36. package/esm/src/cache/cache-key-builder.d.ts +0 -4
  37. package/esm/src/cache/cache-key-builder.d.ts.map +1 -1
  38. package/esm/src/cache/cache-key-builder.js +0 -6
  39. package/esm/src/cache/multi-tier.d.ts +0 -29
  40. package/esm/src/cache/multi-tier.d.ts.map +1 -1
  41. package/esm/src/cache/multi-tier.js +0 -26
  42. package/esm/src/cli/app/actions.d.ts +26 -0
  43. package/esm/src/cli/app/actions.d.ts.map +1 -0
  44. package/esm/src/cli/app/actions.js +152 -0
  45. package/esm/src/cli/app/components/inline-input.d.ts +35 -0
  46. package/esm/src/cli/app/components/inline-input.d.ts.map +1 -0
  47. package/esm/src/cli/app/components/inline-input.js +220 -0
  48. package/esm/src/cli/app/components/list-select.d.ts +69 -0
  49. package/esm/src/cli/app/components/list-select.d.ts.map +1 -0
  50. package/esm/src/cli/app/components/list-select.js +137 -0
  51. package/esm/src/cli/app/index.d.ts +45 -0
  52. package/esm/src/cli/app/index.d.ts.map +1 -0
  53. package/esm/src/cli/app/index.js +1252 -0
  54. package/esm/src/cli/app/state.d.ts +122 -0
  55. package/esm/src/cli/app/state.d.ts.map +1 -0
  56. package/esm/src/cli/app/state.js +232 -0
  57. package/esm/src/cli/app/views/dashboard.d.ts +19 -0
  58. package/esm/src/cli/app/views/dashboard.d.ts.map +1 -0
  59. package/esm/src/cli/app/views/dashboard.js +178 -0
  60. package/esm/src/cli/index/command-router.d.ts.map +1 -1
  61. package/esm/src/cli/index/command-router.js +9 -39
  62. package/esm/src/cli/index/start-handler.d.ts +3 -0
  63. package/esm/src/cli/index/start-handler.d.ts.map +1 -0
  64. package/esm/src/cli/index/start-handler.js +145 -0
  65. package/esm/src/cli/mcp/index.d.ts +11 -0
  66. package/esm/src/cli/mcp/index.d.ts.map +1 -0
  67. package/esm/src/cli/mcp/index.js +10 -0
  68. package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts +2 -0
  69. package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts.map +1 -1
  70. package/esm/src/middleware/builtin/security/redis-rate-limit.js +23 -9
  71. package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts +10 -0
  72. package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts.map +1 -1
  73. package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.js +30 -42
  74. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  75. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +34 -13
  76. package/esm/src/platform/adapters/fs/cache/file-cache.d.ts.map +1 -1
  77. package/esm/src/platform/adapters/fs/cache/file-cache.js +9 -3
  78. package/esm/src/server/context/cache-invalidation.d.ts.map +1 -1
  79. package/esm/src/server/context/cache-invalidation.js +4 -0
  80. package/esm/src/server/handlers/dev/dashboard/api.js +4 -0
  81. package/esm/src/server/handlers/dev/projects/ui-handler.d.ts.map +1 -1
  82. package/esm/src/server/handlers/dev/projects/ui-handler.js +6 -0
  83. package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
  84. package/esm/src/transforms/esm/http-cache.js +139 -64
  85. package/esm/src/utils/index.d.ts +1 -1
  86. package/esm/src/utils/index.d.ts.map +1 -1
  87. package/esm/src/utils/index.js +1 -1
  88. package/package.json +2 -1
  89. package/src/deno.js +1 -1
  90. package/src/proxy/cache/index.ts +93 -0
  91. package/src/proxy/cache/memory-cache.ts +120 -0
  92. package/src/proxy/cache/redis-cache.ts +203 -0
  93. package/src/proxy/cache/resilient-cache.ts +205 -0
  94. package/src/proxy/cache/types.ts +72 -0
  95. package/src/proxy/handler.ts +593 -0
  96. package/src/proxy/logger.ts +329 -0
  97. package/src/proxy/oauth-client.ts +91 -0
  98. package/src/proxy/token-manager.ts +174 -0
  99. package/src/proxy/tracing.ts +237 -0
  100. package/src/src/cache/backend.ts +3 -0
  101. package/src/src/cache/cache-key-builder.ts +0 -9
  102. package/src/src/cache/multi-tier.ts +0 -41
  103. package/src/src/cli/app/actions.ts +190 -0
  104. package/src/src/cli/app/components/inline-input.ts +255 -0
  105. package/src/src/cli/app/components/list-select.ts +215 -0
  106. package/src/src/cli/app/index.ts +1471 -0
  107. package/src/src/cli/app/state.ts +385 -0
  108. package/src/src/cli/app/views/dashboard.ts +212 -0
  109. package/src/src/cli/index/command-router.ts +9 -40
  110. package/src/src/cli/index/start-handler.ts +195 -0
  111. package/src/src/cli/mcp/index.ts +11 -0
  112. package/src/src/middleware/builtin/security/redis-rate-limit.ts +24 -11
  113. package/src/src/modules/react-loader/ssr-module-loader/cache/redis.ts +36 -50
  114. package/src/src/modules/react-loader/ssr-module-loader/loader.ts +38 -14
  115. package/src/src/platform/adapters/fs/cache/file-cache.ts +9 -3
  116. package/src/src/server/context/cache-invalidation.ts +4 -0
  117. package/src/src/server/handlers/dev/dashboard/api.ts +2 -0
  118. package/src/src/server/handlers/dev/projects/ui-handler.ts +6 -0
  119. package/src/src/transforms/esm/http-cache.ts +148 -73
  120. package/src/src/utils/index.ts +0 -1
@@ -0,0 +1,255 @@
1
+ /****
2
+ * Inline Text Input Component
3
+ *
4
+ * Renders an input prompt at the bottom of the TUI that stays inline
5
+ * without exiting alternate screen mode.
6
+ */
7
+
8
+ import { brand, dim, muted } from "../../ui/colors.js";
9
+ import type { InputState, LogEntry } from "../state.js";
10
+
11
+ export interface InlineInputOptions {
12
+ maxWidth?: number;
13
+ }
14
+
15
+ /**
16
+ * Render the inline input prompt
17
+ */
18
+ export function renderInput(input: InputState, _options: InlineInputOptions = {}): string {
19
+ if (!input.active) return "";
20
+
21
+ // Build the input line with cursor
22
+ const prompt = ` ${brand(">")} ${input.prompt}: `;
23
+ const beforeCursor = input.value.slice(0, input.cursorPos);
24
+ const cursorChar = input.value[input.cursorPos] ?? " ";
25
+ const afterCursor = input.value.slice(input.cursorPos + 1);
26
+
27
+ // Cursor is rendered as inverse video
28
+ const cursor = `\x1b[7m${cursorChar}\x1b[27m`;
29
+
30
+ const inputLine = `${prompt}${beforeCursor}${cursor}${afterCursor}`;
31
+
32
+ // Hint line
33
+ const hintLine = ` ${dim("Enter")} ${muted("to submit")} ${dim("Esc")} ${muted("to cancel")}`;
34
+
35
+ return `${inputLine}\n${hintLine}`;
36
+ }
37
+
38
+ export interface RenderLogsOptions {
39
+ maxLines?: number;
40
+ maxWidth?: number;
41
+ scroll?: number;
42
+ expanded?: boolean;
43
+ }
44
+
45
+ /**
46
+ * Render the logs area with optional scrolling
47
+ */
48
+ export function renderLogs(
49
+ logs: LogEntry[],
50
+ options: RenderLogsOptions = {},
51
+ ): string {
52
+ const { maxLines = 5, maxWidth = 80, scroll = 0, expanded = false } = options;
53
+
54
+ if (logs.length === 0) return "";
55
+
56
+ const visibleLines = expanded ? Math.max(maxLines, 15) : maxLines;
57
+ const end = logs.length - scroll;
58
+ const start = Math.max(0, end - visibleLines);
59
+ const visibleLogs = logs.slice(start, end);
60
+
61
+ const lines: string[] = [];
62
+
63
+ for (const log of visibleLogs) {
64
+ const time = formatTime(log.time);
65
+ const levelColor = getLevelColor(log.level);
66
+ const levelPrefix = getLevelPrefix(log.level);
67
+
68
+ if (expanded) {
69
+ // When expanded, show structured info if available
70
+ if (log.meta?.method) {
71
+ // Request log - show all details in clean format
72
+ const meta = log.meta;
73
+ const statusColor = getStatusColor(meta.status || 200);
74
+ const methodStr = (meta.method || "GET").padEnd(7);
75
+ const pathStr = meta.path || "/";
76
+ const statusStr = String(meta.status || 200);
77
+ const durationStr = `${meta.durationMs || 0}ms`.padStart(6);
78
+
79
+ // Line 1: time + method + path
80
+ lines.push(
81
+ ` ${dim(time)} ${levelColor(levelPrefix)} ${methodStr}${pathStr}`,
82
+ );
83
+
84
+ // Line 2: status + duration + project info
85
+ const projectInfo: string[] = [];
86
+ if (meta.project) projectInfo.push(brand(meta.project));
87
+ if (meta.env) projectInfo.push(dim(meta.env));
88
+ if (meta.releaseId) projectInfo.push(dim(`#${meta.releaseId.slice(0, 8)}`));
89
+
90
+ lines.push(
91
+ ` ${"".padEnd(12)}${statusColor(statusStr)} ${dim(durationStr)}${
92
+ projectInfo.length ? ` ${projectInfo.join(" ")}` : ""
93
+ }`,
94
+ );
95
+ } else {
96
+ // Regular log - show full message (may wrap to multiple lines)
97
+ const prefix = ` ${dim(time)} ${levelColor(levelPrefix)} `;
98
+ const msgLines = wrapText(log.message, maxWidth - 15);
99
+ lines.push(`${prefix}${msgLines[0] || ""}`);
100
+ // Indent continuation lines
101
+ for (let i = 1; i < msgLines.length; i++) {
102
+ lines.push(` ${"".padEnd(12)}${msgLines[i]}`);
103
+ }
104
+ }
105
+ } else {
106
+ // When collapsed, truncate
107
+ const maxMsgLen = maxWidth - 15;
108
+ const msg = log.message.length > maxMsgLen
109
+ ? `${log.message.slice(0, maxMsgLen - 3)}...`
110
+ : log.message;
111
+ lines.push(` ${dim(time)} ${levelColor(levelPrefix)} ${msg}`);
112
+ }
113
+ }
114
+
115
+ if (expanded && logs.length > visibleLines) {
116
+ const canScrollUp = start > 0;
117
+ const canScrollDown = scroll > 0;
118
+ const scrollHint = [];
119
+ if (canScrollUp) scrollHint.push("↑");
120
+ if (canScrollDown) scrollHint.push("↓");
121
+ if (scrollHint.length > 0) {
122
+ lines.push(` ${dim(`[${scrollHint.join(" ")}] ${logs.length} total`)}`);
123
+ }
124
+ }
125
+
126
+ return lines.join("\n");
127
+ }
128
+
129
+ function wrapText(text: string, maxWidth: number): string[] {
130
+ if (text.length <= maxWidth) return [text];
131
+
132
+ const lines: string[] = [];
133
+ let remaining = text;
134
+
135
+ while (remaining.length > maxWidth) {
136
+ let breakPoint = remaining.lastIndexOf(" ", maxWidth);
137
+ if (breakPoint <= 0) breakPoint = maxWidth;
138
+ lines.push(remaining.slice(0, breakPoint));
139
+ remaining = remaining.slice(breakPoint).trimStart();
140
+ }
141
+
142
+ if (remaining) lines.push(remaining);
143
+ return lines;
144
+ }
145
+
146
+ /**
147
+ * Format time as HH:MM:SS
148
+ */
149
+ function formatTime(date: Date): string {
150
+ const h = String(date.getHours()).padStart(2, "0");
151
+ const m = String(date.getMinutes()).padStart(2, "0");
152
+ const s = String(date.getSeconds()).padStart(2, "0");
153
+ return `${h}:${m}:${s}`;
154
+ }
155
+
156
+ /**
157
+ * Get color function for log level
158
+ */
159
+ function getLevelColor(level: LogEntry["level"]): (s: string) => string {
160
+ switch (level) {
161
+ case "error":
162
+ return (s: string) => `\x1b[31m${s}\x1b[0m`; // red
163
+ case "warn":
164
+ return (s: string) => `\x1b[33m${s}\x1b[0m`; // yellow
165
+ case "info":
166
+ return (s: string) => `\x1b[36m${s}\x1b[0m`; // cyan
167
+ case "debug":
168
+ return dim;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Get color function for HTTP status code
174
+ */
175
+ function getStatusColor(status: number): (s: string) => string {
176
+ if (status >= 500) return (s: string) => `\x1b[31m${s}\x1b[0m`; // red
177
+ if (status >= 400) return (s: string) => `\x1b[33m${s}\x1b[0m`; // yellow
178
+ if (status >= 300) return (s: string) => `\x1b[36m${s}\x1b[0m`; // cyan
179
+ return (s: string) => `\x1b[32m${s}\x1b[0m`; // green
180
+ }
181
+
182
+ /**
183
+ * Get prefix for log level
184
+ */
185
+ function getLevelPrefix(level: LogEntry["level"]): string {
186
+ switch (level) {
187
+ case "error":
188
+ return "ERR";
189
+ case "warn":
190
+ return "WRN";
191
+ case "info":
192
+ return "INF";
193
+ case "debug":
194
+ return "DBG";
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Handle input key press
200
+ * Returns the new value and cursor position, or null if the key should end input
201
+ */
202
+ export function handleInputKey(
203
+ key: string,
204
+ value: string,
205
+ cursorPos: number,
206
+ ): { value: string; cursorPos: number } | { action: "submit" | "cancel" } {
207
+ if (key === "\r" || key === "\n") return { action: "submit" };
208
+ if (key === "\x1b" || key === "\x03") return { action: "cancel" };
209
+
210
+ if (key === "\x7f" || key === "\b") {
211
+ if (cursorPos === 0) return { value, cursorPos };
212
+
213
+ return {
214
+ value: value.slice(0, cursorPos - 1) + value.slice(cursorPos),
215
+ cursorPos: cursorPos - 1,
216
+ };
217
+ }
218
+
219
+ if (key === "\x1b[3~") {
220
+ if (cursorPos >= value.length) return { value, cursorPos };
221
+
222
+ return {
223
+ value: value.slice(0, cursorPos) + value.slice(cursorPos + 1),
224
+ cursorPos,
225
+ };
226
+ }
227
+
228
+ if (key === "\x1b[D") return { value, cursorPos: Math.max(0, cursorPos - 1) };
229
+ if (key === "\x1b[C") return { value, cursorPos: Math.min(value.length, cursorPos + 1) };
230
+ if (key === "\x01" || key === "\x1b[H") return { value, cursorPos: 0 };
231
+ if (key === "\x05" || key === "\x1b[F") return { value, cursorPos: value.length };
232
+ if (key === "\x15") return { value: "", cursorPos: 0 };
233
+
234
+ if (key === "\x17") {
235
+ if (cursorPos === 0) return { value, cursorPos };
236
+
237
+ let newPos = cursorPos - 1;
238
+ while (newPos > 0 && value[newPos] === " ") newPos--;
239
+ while (newPos > 0 && value[newPos - 1] !== " ") newPos--;
240
+
241
+ return {
242
+ value: value.slice(0, newPos) + value.slice(cursorPos),
243
+ cursorPos: newPos,
244
+ };
245
+ }
246
+
247
+ if (key.length === 1 && key >= " " && key <= "~") {
248
+ return {
249
+ value: value.slice(0, cursorPos) + key + value.slice(cursorPos),
250
+ cursorPos: cursorPos + 1,
251
+ };
252
+ }
253
+
254
+ return { value, cursorPos };
255
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Interactive List Select Component
3
+ *
4
+ * Keyboard-navigable list with selection support.
5
+ * Supports arrow keys, j/k vim bindings, and number shortcuts.
6
+ */
7
+
8
+ import { brand, dim } from "../../ui/colors.js";
9
+ import { truncate } from "../../ui/layout.js";
10
+
11
+ export interface ListItem<T = unknown> {
12
+ /** Unique identifier */
13
+ id: string;
14
+ /** Display label */
15
+ label: string;
16
+ /** Optional description */
17
+ description?: string;
18
+ /** Optional path or metadata */
19
+ meta?: string;
20
+ /** Associated data */
21
+ data?: T;
22
+ }
23
+
24
+ export interface ListSelectOptions {
25
+ /** Maximum width for the list */
26
+ maxWidth?: number;
27
+ /** Number of visible items (for scrolling) */
28
+ visibleCount?: number;
29
+ /** Show number shortcuts (1-9) */
30
+ showNumbers?: boolean;
31
+ /** Offset for number shortcuts (e.g., 1 means start at [2]) */
32
+ numberOffset?: number;
33
+ /** Empty state message */
34
+ emptyMessage?: string;
35
+ /** Show selection cursor (default true). Set false for inactive sections */
36
+ showSelection?: boolean;
37
+ }
38
+
39
+ export interface ListSelectState<T = unknown> {
40
+ /** All items in the list */
41
+ items: ListItem<T>[];
42
+ /** Currently selected index */
43
+ selectedIndex: number;
44
+ /** Scroll offset for long lists */
45
+ scrollOffset: number;
46
+ }
47
+
48
+ /**
49
+ * Create initial list state
50
+ */
51
+ export function createListState<T>(items: ListItem<T>[]): ListSelectState<T> {
52
+ return {
53
+ items,
54
+ selectedIndex: 0,
55
+ scrollOffset: 0,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Move selection up
61
+ */
62
+ export function moveUp<T>(state: ListSelectState<T>): ListSelectState<T> {
63
+ if (state.items.length === 0) return state;
64
+
65
+ const newIndex = state.selectedIndex > 0 ? state.selectedIndex - 1 : state.items.length - 1;
66
+
67
+ const scrollOffset = newIndex < state.scrollOffset ? newIndex : state.scrollOffset;
68
+
69
+ return { ...state, selectedIndex: newIndex, scrollOffset };
70
+ }
71
+
72
+ /**
73
+ * Move selection down
74
+ */
75
+ export function moveDown<T>(
76
+ state: ListSelectState<T>,
77
+ visibleCount = 10,
78
+ ): ListSelectState<T> {
79
+ if (state.items.length === 0) return state;
80
+
81
+ const newIndex = state.selectedIndex < state.items.length - 1 ? state.selectedIndex + 1 : 0;
82
+
83
+ let scrollOffset = state.scrollOffset;
84
+
85
+ if (newIndex === 0) {
86
+ scrollOffset = 0;
87
+ } else if (newIndex >= scrollOffset + visibleCount) {
88
+ scrollOffset = newIndex - visibleCount + 1;
89
+ }
90
+
91
+ return { ...state, selectedIndex: newIndex, scrollOffset };
92
+ }
93
+
94
+ /**
95
+ * Select item by number (1-9)
96
+ */
97
+ export function selectByNumber<T>(
98
+ state: ListSelectState<T>,
99
+ num: number,
100
+ ): ListSelectState<T> {
101
+ const index = num - 1;
102
+ if (index < 0 || index >= state.items.length) return state;
103
+ return { ...state, selectedIndex: index };
104
+ }
105
+
106
+ /**
107
+ * Get currently selected item
108
+ */
109
+ export function getSelectedItem<T>(
110
+ state: ListSelectState<T>,
111
+ ): ListItem<T> | undefined {
112
+ return state.items[state.selectedIndex];
113
+ }
114
+
115
+ /**
116
+ * Render the list as a string
117
+ */
118
+ export function renderList<T>(
119
+ state: ListSelectState<T>,
120
+ options: ListSelectOptions = {},
121
+ ): string {
122
+ const {
123
+ maxWidth = 60,
124
+ visibleCount = 10,
125
+ showNumbers = true,
126
+ numberOffset = 0,
127
+ emptyMessage = "No items",
128
+ showSelection = true,
129
+ } = options;
130
+
131
+ if (state.items.length === 0) return ` ${dim(emptyMessage)}`;
132
+
133
+ const lines: string[] = [];
134
+ const start = state.scrollOffset;
135
+ const end = Math.min(start + visibleCount, state.items.length);
136
+ const visibleItems = state.items.slice(start, end);
137
+
138
+ const numberWidth = showNumbers ? 4 : 0; // " [1] "
139
+ const cursorWidth = 2; // "› " or " "
140
+ const prefixWidth = numberWidth + cursorWidth;
141
+
142
+ for (let i = 0; i < visibleItems.length; i++) {
143
+ const item = visibleItems[i];
144
+ if (!item) continue;
145
+
146
+ const actualIndex = start + i;
147
+ const isSelected = showSelection && actualIndex === state.selectedIndex;
148
+ const displayNum = actualIndex + 1 + numberOffset;
149
+
150
+ const parts: string[] = [];
151
+
152
+ parts.push(isSelected ? brand("›") : " ", " ");
153
+
154
+ if (showNumbers) {
155
+ if (displayNum <= 35) {
156
+ const shortcut = displayNum <= 9
157
+ ? String(displayNum)
158
+ : String.fromCharCode(96 + displayNum - 9); // 10='a', 11='b', etc.
159
+ parts.push(isSelected ? brand(`[${shortcut}]`) : dim(`[${shortcut}]`), " ");
160
+ } else {
161
+ parts.push(" ");
162
+ }
163
+ }
164
+
165
+ // Render label, then use remaining space for meta
166
+ const labelText = item.label;
167
+ const availableForContent = maxWidth - prefixWidth;
168
+
169
+ if (item.meta) {
170
+ // Split space between label and meta dynamically
171
+ const metaText = item.meta;
172
+ const totalNeeded = labelText.length + 1 + metaText.length; // 1 for space
173
+
174
+ if (totalNeeded <= availableForContent) {
175
+ // Both fit - no truncation needed
176
+ parts.push(isSelected ? labelText : dim(labelText));
177
+ const padding = availableForContent - labelText.length - metaText.length;
178
+ parts.push(" ".repeat(Math.max(1, padding)), dim(metaText));
179
+ } else {
180
+ // Need to truncate - prioritize label, give rest to meta
181
+ const labelMax = Math.min(labelText.length, Math.floor(availableForContent * 0.4));
182
+ const metaMax = availableForContent - labelMax - 1;
183
+ const label = truncate(labelText, labelMax);
184
+ parts.push(isSelected ? label : dim(label));
185
+ parts.push(" ", dim(truncate(metaText, metaMax)));
186
+ }
187
+ } else {
188
+ const label = truncate(labelText, availableForContent);
189
+ parts.push(isSelected ? label : dim(label));
190
+ }
191
+
192
+ lines.push(parts.join(""));
193
+
194
+ if (isSelected && item.description) {
195
+ lines.push(` ${dim(truncate(item.description, maxWidth - 5))}`);
196
+ }
197
+ }
198
+
199
+ if (start > 0) lines.unshift(` ${dim("↑ more above")}`);
200
+ if (end < state.items.length) lines.push(` ${dim("↓ more below")}`);
201
+
202
+ return lines.join("\n");
203
+ }
204
+
205
+ /**
206
+ * Create a list section with title
207
+ */
208
+ export function listSection<T>(
209
+ title: string,
210
+ state: ListSelectState<T>,
211
+ options: ListSelectOptions = {},
212
+ ): string {
213
+ const header = ` ${dim(title)} ${dim(`(${state.items.length})`)}`;
214
+ return `${header}\n${renderList(state, options)}`;
215
+ }