@ebowwa/coder 0.7.66 → 0.7.68

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.
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Scroll Handler - Chat History Scrolling
3
+ *
4
+ * Manages scroll state for the chat message history.
5
+ * Scroll offset represents how many messages to "hide" from the bottom (newer messages).
6
+ *
7
+ * scrollOffset = 0 -> Show latest messages (bottom)
8
+ * scrollOffset > 0 -> Show older messages (scrolled up)
9
+ */
10
+
11
+ import type { NativeKeyEvent } from "./types.js";
12
+ import { KeyEvents } from "./input-handler.js";
13
+
14
+ // ============================================
15
+ // TYPES
16
+ // ============================================
17
+
18
+ export interface ScrollState {
19
+ /** Number of messages hidden from bottom (0 = show latest) */
20
+ offset: number;
21
+ }
22
+
23
+ export interface ScrollConfig {
24
+ /** Messages to scroll per PageUp/PageDown */
25
+ pageScrollAmount: number;
26
+ /** Messages to scroll per Shift+Up/Down */
27
+ lineScrollAmount: number;
28
+ }
29
+
30
+ // ============================================
31
+ // DEFAULTS
32
+ // ============================================
33
+
34
+ export const DEFAULT_SCROLL_CONFIG: ScrollConfig = {
35
+ pageScrollAmount: 3,
36
+ lineScrollAmount: 1,
37
+ };
38
+
39
+ // ============================================
40
+ // SCROLL HANDLER CLASS
41
+ // ============================================
42
+
43
+ /**
44
+ * Handles chat history scrolling with keyboard input
45
+ *
46
+ * Usage:
47
+ * ```ts
48
+ * const scrollHandler = new ScrollHandler();
49
+ *
50
+ * // In input handler
51
+ * const result = scrollHandler.handleKeyEvent(event, totalMessages);
52
+ * if (result.handled) {
53
+ * state.scrollOffset = result.newOffset;
54
+ * }
55
+ * ```
56
+ */
57
+ export class ScrollHandler {
58
+ private config: ScrollConfig;
59
+ private _offset: number = 0;
60
+
61
+ constructor(config: Partial<ScrollConfig> = {}) {
62
+ this.config = { ...DEFAULT_SCROLL_CONFIG, ...config };
63
+ }
64
+
65
+ /** Current scroll offset */
66
+ get offset(): number {
67
+ return this._offset;
68
+ }
69
+
70
+ /** Set scroll offset directly */
71
+ set offset(value: number) {
72
+ this._offset = Math.max(0, value);
73
+ }
74
+
75
+ /**
76
+ * Handle a keyboard event for scrolling
77
+ * Returns the new offset and whether it event was handled
78
+ */
79
+ handleKeyEvent(
80
+ event: NativeKeyEvent,
81
+ totalMessages: number
82
+ ): { handled: boolean; newOffset: number } {
83
+ // Shift+Up = scroll up (show older messages, increase offset)
84
+ if (KeyEvents.isUp(event) && event.shift) {
85
+ const maxScroll = this.calculateMaxScroll(totalMessages);
86
+ const newOffset = Math.min(this._offset + this.config.lineScrollAmount, maxScroll);
87
+ this._offset = newOffset;
88
+ return { handled: true, newOffset };
89
+ }
90
+
91
+ // Shift+Down = scroll down (show newer messages, decrease offset)
92
+ if (KeyEvents.isDown(event) && event.shift) {
93
+ const newOffset = Math.max(0, this._offset - this.config.lineScrollAmount);
94
+ this._offset = newOffset;
95
+ return { handled: true, newOffset };
96
+ }
97
+
98
+ // PageUp = scroll up by page amount
99
+ if (KeyEvents.isPageUp(event)) {
100
+ const maxScroll = this.calculateMaxScroll(totalMessages);
101
+ const newOffset = Math.min(this._offset + this.config.pageScrollAmount, maxScroll);
102
+ this._offset = newOffset;
103
+ return { handled: true, newOffset };
104
+ }
105
+
106
+ // PageDown = scroll down by page amount
107
+ if (KeyEvents.isPageDown(event)) {
108
+ const newOffset = Math.max(0, this._offset - this.config.pageScrollAmount);
109
+ this._offset = newOffset;
110
+ return { handled: true, newOffset };
111
+ }
112
+
113
+ // Not a scroll event
114
+ return { handled: false, newOffset: this._offset };
115
+ }
116
+
117
+ /**
118
+ * Calculate maximum scroll offset
119
+ * Returns the maximum number of lines you can scroll up
120
+ * This allows scrolling even with a single message
121
+ */
122
+ private calculateMaxScroll(totalMessages: number, estimatedLines: number = 50): number {
123
+ // If we have messages, allow scrolling through all their estimated lines
124
+ // Even 1 message with many lines should be scrollable
125
+ if (totalMessages === 0) return 0;
126
+ // Estimate: each message has ~5 lines on average, minimum 10 lines scrollable
127
+ const estimatedTotalLines = Math.max(totalMessages * 5, 10);
128
+ return Math.max(0, estimatedTotalLines);
129
+ }
130
+
131
+ /**
132
+ * Reset scroll to bottom (show latest)
133
+ */
134
+ reset(): void {
135
+ this._offset = 0;
136
+ }
137
+
138
+ /**
139
+ * Check if currently scrolled (not at bottom)
140
+ */
141
+ get isScrolled(): boolean {
142
+ return this._offset > 0;
143
+ }
144
+
145
+ /**
146
+ * Get scroll info for display
147
+ */
148
+ getScrollInfo(totalMessages: number, visibleCount: number): {
149
+ isScrollable: boolean;
150
+ olderCount: number;
151
+ newerCount: number;
152
+ visibleRange: string;
153
+ } {
154
+ const maxScroll = this.calculateMaxScroll(totalMessages);
155
+ const isScrollable = totalMessages > 1;
156
+
157
+ // How many older messages are hidden above the view
158
+ const olderCount = this._offset;
159
+
160
+ // How many newer messages are hidden below the view
161
+ const newerCount = Math.max(0, totalMessages - visibleCount - olderCount);
162
+
163
+ return {
164
+ isScrollable,
165
+ olderCount,
166
+ newerCount,
167
+ visibleRange: `${visibleCount}/${totalMessages}`,
168
+ };
169
+ }
170
+ }
171
+
172
+ // ============================================
173
+ // STANDALONE FUNCTIONS
174
+ // ============================================
175
+
176
+ /**
177
+ * Handle scroll key events (stateless version)
178
+ * Use this if you prefer functional style over class-based
179
+ *
180
+ * Keybindings:
181
+ * - PageUp: scroll up by page (always works)
182
+ * - PageDown: scroll down by page (always works)
183
+ * - Home: reset to bottom (show latest)
184
+ * - Shift+Up/Down: line scroll (may not work in all terminals)
185
+ * - Alt+Up/Down: line scroll (alternative)
186
+ * - Ctrl+Up/Down: line scroll (alternative)
187
+ */
188
+ export function handleScrollEvent(
189
+ event: NativeKeyEvent,
190
+ currentOffset: number,
191
+ totalMessages: number,
192
+ config: Partial<ScrollConfig> = {}
193
+ ): { handled: boolean; newOffset: number } {
194
+ const { pageScrollAmount, lineScrollAmount } = { ...DEFAULT_SCROLL_CONFIG, ...config };
195
+
196
+ // Calculate max scroll - allow scrolling even with few messages
197
+ // Minimum scroll range of 10 to handle small terminals
198
+ const maxScroll = Math.max(0, Math.max(totalMessages - 1, 10));
199
+
200
+ // Debug: log what we're receiving
201
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
202
+ console.error("[ScrollHandler] Event:", {
203
+ code: event.code,
204
+ shift: event.shift,
205
+ ctrl: event.ctrl,
206
+ alt: event.alt,
207
+ is_special: event.is_special,
208
+ });
209
+ }
210
+
211
+ // PageUp = scroll up by page (always works)
212
+ if (KeyEvents.isPageUp(event)) {
213
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
214
+ console.error("[ScrollHandler] PageUp detected, scrolling up");
215
+ }
216
+ return { handled: true, newOffset: Math.min(currentOffset + pageScrollAmount, maxScroll) };
217
+ }
218
+
219
+ // PageDown = scroll down by page
220
+ if (KeyEvents.isPageDown(event)) {
221
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
222
+ console.error("[ScrollHandler] PageDown detected, scrolling down");
223
+ }
224
+ return { handled: true, newOffset: Math.max(0, currentOffset - pageScrollAmount) };
225
+ }
226
+
227
+ // Home = reset to bottom (show latest)
228
+ if (KeyEvents.isHome(event)) {
229
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
230
+ console.error("[ScrollHandler] Home detected, resetting to bottom");
231
+ }
232
+ return { handled: true, newOffset: 0 };
233
+ }
234
+
235
+ // Alt+Up = scroll up (works in most terminals)
236
+ if (KeyEvents.isUp(event) && event.alt) {
237
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
238
+ console.error("[ScrollHandler] Alt+Up detected, scrolling up");
239
+ }
240
+ return { handled: true, newOffset: Math.min(currentOffset + lineScrollAmount, maxScroll) };
241
+ }
242
+
243
+ // Alt+Down = scroll down
244
+ if (KeyEvents.isDown(event) && event.alt) {
245
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
246
+ console.error("[ScrollHandler] Alt+Down detected, scrolling down");
247
+ }
248
+ return { handled: true, newOffset: Math.max(0, currentOffset - lineScrollAmount) };
249
+ }
250
+
251
+ // Ctrl+Up = scroll up (alternative)
252
+ if (KeyEvents.isUp(event) && event.ctrl) {
253
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
254
+ console.error("[ScrollHandler] Ctrl+Up detected, scrolling up");
255
+ }
256
+ return { handled: true, newOffset: Math.min(currentOffset + lineScrollAmount, maxScroll) };
257
+ }
258
+
259
+ // Ctrl+Down = scroll down (alternative)
260
+ if (KeyEvents.isDown(event) && event.ctrl) {
261
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
262
+ console.error("[ScrollHandler] Ctrl+Down detected, scrolling down");
263
+ }
264
+ return { handled: true, newOffset: Math.max(0, currentOffset - lineScrollAmount) };
265
+ }
266
+
267
+ // Shift+Up = scroll up (fallback, may not work in all terminals)
268
+ if (KeyEvents.isUp(event) && event.shift) {
269
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
270
+ console.error("[ScrollHandler] Shift+Up detected, scrolling up");
271
+ }
272
+ return { handled: true, newOffset: Math.min(currentOffset + lineScrollAmount, maxScroll) };
273
+ }
274
+
275
+ // Shift+Down = scroll down (fallback)
276
+ if (KeyEvents.isDown(event) && event.shift) {
277
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
278
+ console.error("[ScrollHandler] Shift+Down detected, scrolling down");
279
+ }
280
+ return { handled: true, newOffset: Math.max(0, currentOffset - lineScrollAmount) };
281
+ }
282
+
283
+ return { handled: false, newOffset: currentOffset };
284
+ }
@@ -186,6 +186,7 @@ export interface RenderState {
186
186
  searchQuery: string;
187
187
  searchResults: SearchResult[];
188
188
  searchSelected: number;
189
+ scrollOffset?: number;
189
190
  }
190
191
 
191
192
  export interface InputEvent {