@ebowwa/coder 0.7.66 → 0.7.69

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.
@@ -7378,6 +7378,69 @@ function convertToolsToOpenAIFormat(tools) {
7378
7378
  }
7379
7379
  }));
7380
7380
  }
7381
+ function convertMessagesToOpenAIFormat(messages) {
7382
+ const result = [];
7383
+ for (const msg of messages) {
7384
+ if (msg.role === "assistant") {
7385
+ const toolCalls = [];
7386
+ const textParts = [];
7387
+ for (const block of msg.content) {
7388
+ if (block.type === "text") {
7389
+ textParts.push(block.text);
7390
+ } else if (block.type === "tool_use") {
7391
+ toolCalls.push({
7392
+ id: block.id,
7393
+ type: "function",
7394
+ function: {
7395
+ name: block.name,
7396
+ arguments: JSON.stringify(block.input)
7397
+ }
7398
+ });
7399
+ } else if (block.type === "thinking" || block.type === "redacted_thinking") {}
7400
+ }
7401
+ const openAIMsg = {
7402
+ role: "assistant",
7403
+ content: textParts.join(`
7404
+ `) || null
7405
+ };
7406
+ if (toolCalls.length > 0) {
7407
+ openAIMsg.tool_calls = toolCalls;
7408
+ }
7409
+ result.push(openAIMsg);
7410
+ } else if (msg.role === "user") {
7411
+ const textParts = [];
7412
+ const toolResults = [];
7413
+ for (const block of msg.content) {
7414
+ if (block.type === "text") {
7415
+ textParts.push(block.text);
7416
+ } else if (block.type === "tool_result") {
7417
+ const contentStr = typeof block.content === "string" ? block.content : block.content.map((c) => c.type === "text" ? c.text : JSON.stringify(c)).join(`
7418
+ `);
7419
+ toolResults.push({
7420
+ tool_use_id: block.tool_use_id,
7421
+ content: contentStr,
7422
+ is_error: block.is_error
7423
+ });
7424
+ }
7425
+ }
7426
+ if (textParts.length > 0) {
7427
+ result.push({
7428
+ role: "user",
7429
+ content: textParts.join(`
7430
+ `)
7431
+ });
7432
+ }
7433
+ for (const tr of toolResults) {
7434
+ result.push({
7435
+ role: "tool",
7436
+ tool_call_id: tr.tool_use_id,
7437
+ content: tr.content
7438
+ });
7439
+ }
7440
+ }
7441
+ }
7442
+ return result;
7443
+ }
7381
7444
  function calculateCost2(model, usage) {
7382
7445
  return calculateCost(model, usage);
7383
7446
  }
@@ -7664,6 +7727,7 @@ async function executeStreamAttempt(request, headers, apiEndpoint, signal, model
7664
7727
  }
7665
7728
  }
7666
7729
  if (choice?.finish_reason) {
7730
+ console.log(`[API] OpenAI finish_reason: ${choice.finish_reason}, content blocks: ${currentContent.length}, hasToolUse: ${!!currentToolUseBlock}`);
7667
7731
  if (currentTextBlock) {
7668
7732
  currentContent.push(currentTextBlock);
7669
7733
  currentTextBlock = null;
@@ -7771,20 +7835,6 @@ async function createMessageStream(messages, options) {
7771
7835
  signal
7772
7836
  } = options;
7773
7837
  const startTime = Date.now();
7774
- const cachedMessages = buildCachedMessages(messages, cacheConfig);
7775
- const cachedSystemPrompt = buildSystemPrompt(systemPrompt, cacheConfig);
7776
- const request = {
7777
- model,
7778
- max_tokens: maxTokens,
7779
- messages: cachedMessages.map((m) => ({
7780
- role: m.role,
7781
- content: m.content
7782
- })),
7783
- stream: true
7784
- };
7785
- if (cachedSystemPrompt) {
7786
- request.system = cachedSystemPrompt;
7787
- }
7788
7838
  const providerInfo = resolveProvider(model);
7789
7839
  let apiEndpoint;
7790
7840
  let headers;
@@ -7814,12 +7864,48 @@ async function createMessageStream(messages, options) {
7814
7864
  "anthropic-version": "2023-06-01"
7815
7865
  };
7816
7866
  }
7867
+ const cachedMessages = buildCachedMessages(messages, cacheConfig);
7868
+ const cachedSystemPrompt = buildSystemPrompt(systemPrompt, cacheConfig);
7869
+ let requestMessages;
7870
+ if (apiFormat === "openai") {
7871
+ const openAIMessages = convertMessagesToOpenAIFormat(cachedMessages);
7872
+ if (cachedSystemPrompt) {
7873
+ const systemText = typeof cachedSystemPrompt === "string" ? cachedSystemPrompt : cachedSystemPrompt.map((b) => b.text).join(`
7874
+
7875
+ `);
7876
+ requestMessages = [
7877
+ { role: "system", content: systemText },
7878
+ ...openAIMessages
7879
+ ];
7880
+ } else {
7881
+ requestMessages = openAIMessages;
7882
+ }
7883
+ } else {
7884
+ requestMessages = cachedMessages.map((m) => ({
7885
+ role: m.role,
7886
+ content: m.content
7887
+ }));
7888
+ }
7889
+ const request = {
7890
+ model,
7891
+ max_tokens: maxTokens,
7892
+ messages: requestMessages,
7893
+ stream: true
7894
+ };
7895
+ if (cachedSystemPrompt && apiFormat === "anthropic") {
7896
+ request.system = cachedSystemPrompt;
7897
+ }
7817
7898
  if (tools && tools.length > 0) {
7818
7899
  if (apiFormat === "openai") {
7819
- request.tools = convertToolsToOpenAIFormat(tools);
7900
+ const openaiTools = convertToolsToOpenAIFormat(tools);
7901
+ request.tools = openaiTools;
7902
+ console.log(`[API] Sending ${tools.length} tools to ${apiFormat} API:`, JSON.stringify(openaiTools).substring(0, 500));
7820
7903
  } else {
7821
7904
  request.tools = tools;
7905
+ console.log(`[API] Sending ${tools.length} tools to ${apiFormat} API (Anthropic format)`);
7822
7906
  }
7907
+ } else {
7908
+ console.log(`[API] No tools being sent to API`);
7823
7909
  }
7824
7910
  const shouldUseExtendedThinking = (extendedThinking?.enabled ?? false) || thinking && thinking.type !== "disabled";
7825
7911
  if (shouldUseExtendedThinking && supportsExtendedThinking(model)) {
@@ -11249,6 +11335,84 @@ var init_spinner_frames = __esm(() => {
11249
11335
  ];
11250
11336
  });
11251
11337
 
11338
+ // packages/src/interfaces/ui/terminal/cli/interactive/scroll-handler.ts
11339
+ function handleScrollEvent(event, currentOffset, totalMessages, config = {}) {
11340
+ const { pageScrollAmount, lineScrollAmount } = { ...DEFAULT_SCROLL_CONFIG, ...config };
11341
+ const maxScroll = Math.max(0, Math.max(totalMessages - 1, 10));
11342
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
11343
+ console.error("[ScrollHandler] Event:", {
11344
+ code: event.code,
11345
+ shift: event.shift,
11346
+ ctrl: event.ctrl,
11347
+ alt: event.alt,
11348
+ is_special: event.is_special
11349
+ });
11350
+ }
11351
+ if (KeyEvents.isPageUp(event)) {
11352
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
11353
+ console.error("[ScrollHandler] PageUp detected, scrolling up");
11354
+ }
11355
+ return { handled: true, newOffset: Math.min(currentOffset + pageScrollAmount, maxScroll) };
11356
+ }
11357
+ if (KeyEvents.isPageDown(event)) {
11358
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
11359
+ console.error("[ScrollHandler] PageDown detected, scrolling down");
11360
+ }
11361
+ return { handled: true, newOffset: Math.max(0, currentOffset - pageScrollAmount) };
11362
+ }
11363
+ if (KeyEvents.isHome(event)) {
11364
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
11365
+ console.error("[ScrollHandler] Home detected, resetting to bottom");
11366
+ }
11367
+ return { handled: true, newOffset: 0 };
11368
+ }
11369
+ if (KeyEvents.isUp(event) && event.alt) {
11370
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
11371
+ console.error("[ScrollHandler] Alt+Up detected, scrolling up");
11372
+ }
11373
+ return { handled: true, newOffset: Math.min(currentOffset + lineScrollAmount, maxScroll) };
11374
+ }
11375
+ if (KeyEvents.isDown(event) && event.alt) {
11376
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
11377
+ console.error("[ScrollHandler] Alt+Down detected, scrolling down");
11378
+ }
11379
+ return { handled: true, newOffset: Math.max(0, currentOffset - lineScrollAmount) };
11380
+ }
11381
+ if (KeyEvents.isUp(event) && event.ctrl) {
11382
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
11383
+ console.error("[ScrollHandler] Ctrl+Up detected, scrolling up");
11384
+ }
11385
+ return { handled: true, newOffset: Math.min(currentOffset + lineScrollAmount, maxScroll) };
11386
+ }
11387
+ if (KeyEvents.isDown(event) && event.ctrl) {
11388
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
11389
+ console.error("[ScrollHandler] Ctrl+Down detected, scrolling down");
11390
+ }
11391
+ return { handled: true, newOffset: Math.max(0, currentOffset - lineScrollAmount) };
11392
+ }
11393
+ if (KeyEvents.isUp(event) && event.shift) {
11394
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
11395
+ console.error("[ScrollHandler] Shift+Up detected, scrolling up");
11396
+ }
11397
+ return { handled: true, newOffset: Math.min(currentOffset + lineScrollAmount, maxScroll) };
11398
+ }
11399
+ if (KeyEvents.isDown(event) && event.shift) {
11400
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
11401
+ console.error("[ScrollHandler] Shift+Down detected, scrolling down");
11402
+ }
11403
+ return { handled: true, newOffset: Math.max(0, currentOffset - lineScrollAmount) };
11404
+ }
11405
+ return { handled: false, newOffset: currentOffset };
11406
+ }
11407
+ var DEFAULT_SCROLL_CONFIG;
11408
+ var init_scroll_handler = __esm(() => {
11409
+ init_input_handler();
11410
+ DEFAULT_SCROLL_CONFIG = {
11411
+ pageScrollAmount: 3,
11412
+ lineScrollAmount: 1
11413
+ };
11414
+ });
11415
+
11252
11416
  // packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts
11253
11417
  import process3 from "process";
11254
11418
  function createInitialState() {
@@ -11399,6 +11563,23 @@ class InteractiveRunner {
11399
11563
  this.state = { ...this.state, inputValue: "", cursorPos: 0 };
11400
11564
  return true;
11401
11565
  }
11566
+ if (process3.env.CODER_DEBUG_SCROLL === "1") {
11567
+ console.error("[InteractiveRunner] Key event:", {
11568
+ code: event.code,
11569
+ shift: event.shift,
11570
+ ctrl: event.ctrl,
11571
+ alt: event.alt,
11572
+ is_special: event.is_special
11573
+ });
11574
+ }
11575
+ const scrollResult = handleScrollEvent(event, this.state.scrollOffset, this.messageStore.messages.length);
11576
+ if (process3.env.CODER_DEBUG_SCROLL === "1") {
11577
+ console.error("[InteractiveRunner] Scroll result:", scrollResult);
11578
+ }
11579
+ if (scrollResult.handled) {
11580
+ this.state = { ...this.state, scrollOffset: scrollResult.newOffset };
11581
+ return true;
11582
+ }
11402
11583
  if (KeyEvents.isUp(event)) {
11403
11584
  return this._handleHistoryUp();
11404
11585
  }
@@ -11707,7 +11888,8 @@ class InteractiveRunner {
11707
11888
  searchMode: sessionSelectMode,
11708
11889
  searchQuery: "",
11709
11890
  searchResults,
11710
- searchSelected: 0
11891
+ searchSelected: 0,
11892
+ scrollOffset: this.state.scrollOffset
11711
11893
  };
11712
11894
  }
11713
11895
  _getHelpText(section) {
@@ -11809,6 +11991,7 @@ var init_interactive_runner = __esm(() => {
11809
11991
  init_native();
11810
11992
  init_spinner_frames();
11811
11993
  init_input_handler();
11994
+ init_scroll_handler();
11812
11995
  init_types();
11813
11996
  });
11814
11997
 
@@ -34325,3 +34508,6 @@ if (checkPtyWrapper2()) {
34325
34508
  process5.exit(1);
34326
34509
  });
34327
34510
  }
34511
+
34512
+ //# debugId=1ADD58DCF208A7BB64756E2164756E21
34513
+ //# sourceMappingURL=bootstrap.js.map
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ebowwa/coder",
3
- "version": "0.7.66",
3
+ "version": "0.7.69",
4
4
  "description": "AI-powered terminal coding assistant",
5
5
  "author": "ebowwa",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "scripts": {
26
26
  "build": "bun run build:native && bun run build:ts && bun run link",
27
- "build:ts": "bun build ./packages/src/index.ts ./packages/src/interfaces/ui/terminal/cli/bootstrap.ts --outdir ./dist --target bun --external @ebowwa/coder-native && cp -r native dist/ && mkdir -p dist/interfaces/ui/terminal && cp -r native dist/interfaces/ui/terminal/ && mv dist/interfaces/ui/terminal/cli/bootstrap.js dist/interfaces/ui/terminal/cli/index.js",
27
+ "build:ts": "bun build ./packages/src/index.ts ./packages/src/interfaces/ui/terminal/cli/bootstrap.ts --outdir ./dist --target bun --external @ebowwa/coder-native --sourcemap && cp -r native dist/ && mkdir -p dist/interfaces/ui/terminal && cp -r native dist/interfaces/ui/terminal/ && mv dist/interfaces/ui/terminal/cli/bootstrap.js dist/interfaces/ui/terminal/cli/index.js",
28
28
  "build:native": "cd packages/rust && cargo build --release --target-dir ./target && cd ../.. && bun run build:napi-artifacts",
29
29
  "build:napi-artifacts": "napi build --platform --release --target-dir packages/rust/target --manifest-path packages/rust/Cargo.toml --output-dir native --js-package-name @ebowwa/coder-native --const-enum --strip --dts index.d.ts",
30
30
  "link": "bun link || npm link",
@@ -63,6 +63,115 @@ function convertToolsToOpenAIFormat(tools: APITool[]): unknown[] {
63
63
  }));
64
64
  }
65
65
 
66
+ /**
67
+ * OpenAI-format message types
68
+ */
69
+ interface OpenAIToolCall {
70
+ id: string;
71
+ type: "function";
72
+ function: {
73
+ name: string;
74
+ arguments: string;
75
+ };
76
+ }
77
+
78
+ interface OpenAIMessage {
79
+ role: "user" | "assistant" | "tool" | "system";
80
+ content: string | null;
81
+ tool_calls?: OpenAIToolCall[];
82
+ tool_call_id?: string;
83
+ name?: string;
84
+ }
85
+
86
+ /**
87
+ * Convert Anthropic-style messages to OpenAI-style messages
88
+ *
89
+ * Key conversions:
90
+ * 1. Assistant messages with tool_use → add tool_calls array
91
+ * 2. User messages with tool_result → separate role: "tool" messages
92
+ *
93
+ * This is required because OpenAI-format APIs (Zhipu, etc.) don't understand
94
+ * Anthropic's tool_result content block type.
95
+ */
96
+ function convertMessagesToOpenAIFormat(messages: Message[]): OpenAIMessage[] {
97
+ const result: OpenAIMessage[] = [];
98
+
99
+ for (const msg of messages) {
100
+ if (msg.role === "assistant") {
101
+ // Assistant message - check for tool_use blocks
102
+ const toolCalls: OpenAIToolCall[] = [];
103
+ const textParts: string[] = [];
104
+
105
+ for (const block of msg.content) {
106
+ if (block.type === "text") {
107
+ textParts.push(block.text);
108
+ } else if (block.type === "tool_use") {
109
+ toolCalls.push({
110
+ id: block.id,
111
+ type: "function",
112
+ function: {
113
+ name: block.name,
114
+ arguments: JSON.stringify(block.input),
115
+ },
116
+ });
117
+ } else if (block.type === "thinking" || block.type === "redacted_thinking") {
118
+ // Skip thinking blocks in OpenAI format (not supported)
119
+ }
120
+ }
121
+
122
+ const openAIMsg: OpenAIMessage = {
123
+ role: "assistant",
124
+ content: textParts.join("\n") || null,
125
+ };
126
+
127
+ if (toolCalls.length > 0) {
128
+ openAIMsg.tool_calls = toolCalls;
129
+ }
130
+
131
+ result.push(openAIMsg);
132
+ } else if (msg.role === "user") {
133
+ // User message - check for tool_result blocks
134
+ const textParts: string[] = [];
135
+ const toolResults: { tool_use_id: string; content: string; is_error?: boolean }[] = [];
136
+
137
+ for (const block of msg.content) {
138
+ if (block.type === "text") {
139
+ textParts.push(block.text);
140
+ } else if (block.type === "tool_result") {
141
+ // Extract content as string
142
+ const contentStr = typeof block.content === "string"
143
+ ? block.content
144
+ : block.content.map(c => c.type === "text" ? c.text : JSON.stringify(c)).join("\n");
145
+ toolResults.push({
146
+ tool_use_id: block.tool_use_id,
147
+ content: contentStr,
148
+ is_error: block.is_error,
149
+ });
150
+ }
151
+ }
152
+
153
+ // Add text content as user message if present
154
+ if (textParts.length > 0) {
155
+ result.push({
156
+ role: "user",
157
+ content: textParts.join("\n"),
158
+ });
159
+ }
160
+
161
+ // Add each tool result as a separate "tool" role message
162
+ for (const tr of toolResults) {
163
+ result.push({
164
+ role: "tool",
165
+ tool_call_id: tr.tool_use_id,
166
+ content: tr.content,
167
+ });
168
+ }
169
+ }
170
+ }
171
+
172
+ return result;
173
+ }
174
+
66
175
  export interface StreamOptions {
67
176
  apiKey: string;
68
177
  model?: string;
@@ -519,6 +628,9 @@ async function executeStreamAttempt(
519
628
 
520
629
  // Handle finish reason
521
630
  if (choice?.finish_reason) {
631
+ // DEBUG: Log finish reason
632
+ console.log(`[API] OpenAI finish_reason: ${choice.finish_reason}, content blocks: ${currentContent.length}, hasToolUse: ${!!currentToolUseBlock}`);
633
+
522
634
  // Finalize any pending text block
523
635
  if (currentTextBlock) {
524
636
  currentContent.push(currentTextBlock);
@@ -647,30 +759,7 @@ export async function createMessageStream(
647
759
 
648
760
  const startTime = Date.now();
649
761
 
650
- // Build cached messages
651
- const cachedMessages = buildCachedMessages(messages, cacheConfig);
652
-
653
- // Build system prompt with cache control
654
- const cachedSystemPrompt = buildSystemPrompt(systemPrompt, cacheConfig);
655
-
656
- // Build request
657
- const request: APIRequest = {
658
- model,
659
- max_tokens: maxTokens,
660
- messages: cachedMessages.map((m) => ({
661
- role: m.role,
662
- content: m.content,
663
- })),
664
- stream: true,
665
- };
666
-
667
- if (cachedSystemPrompt) {
668
- request.system = cachedSystemPrompt;
669
- }
670
-
671
- // Tools will be set after determining API format (for format conversion)
672
-
673
- // Resolve provider based on model name
762
+ // Resolve provider FIRST to determine API format
674
763
  const providerInfo = resolveProvider(model);
675
764
 
676
765
  // Determine API endpoint and headers based on provider
@@ -710,16 +799,68 @@ export async function createMessageStream(
710
799
  };
711
800
  }
712
801
 
802
+ // Build cached messages
803
+ const cachedMessages = buildCachedMessages(messages, cacheConfig);
804
+
805
+ // Build system prompt with cache control
806
+ const cachedSystemPrompt = buildSystemPrompt(systemPrompt, cacheConfig);
807
+
808
+ // Build request with format-appropriate message conversion
809
+ let requestMessages: unknown;
810
+ if (apiFormat === "openai") {
811
+ // Convert to OpenAI format (handles tool_use and tool_result properly)
812
+ const openAIMessages = convertMessagesToOpenAIFormat(cachedMessages);
813
+
814
+ // Add system prompt as first message for OpenAI format
815
+ if (cachedSystemPrompt) {
816
+ const systemText = typeof cachedSystemPrompt === "string"
817
+ ? cachedSystemPrompt
818
+ : cachedSystemPrompt.map(b => b.text).join("\n\n");
819
+ requestMessages = [
820
+ { role: "system", content: systemText },
821
+ ...openAIMessages,
822
+ ];
823
+ } else {
824
+ requestMessages = openAIMessages;
825
+ }
826
+ } else {
827
+ // Keep Anthropic format
828
+ requestMessages = cachedMessages.map((m) => ({
829
+ role: m.role,
830
+ content: m.content,
831
+ }));
832
+ }
833
+
834
+ // Build request
835
+ const request: APIRequest = {
836
+ model,
837
+ max_tokens: maxTokens,
838
+ messages: requestMessages as Message[],
839
+ stream: true,
840
+ };
841
+
842
+ if (cachedSystemPrompt && apiFormat === "anthropic") {
843
+ // Anthropic format uses separate system field
844
+ // OpenAI format already has system as first message
845
+ request.system = cachedSystemPrompt;
846
+ }
847
+
713
848
  // Set tools with format conversion if needed
714
849
  if (tools && tools.length > 0) {
715
850
  if (apiFormat === "openai") {
716
851
  // Convert Anthropic-style tools to OpenAI format
717
852
  // Cast needed because OpenAI format differs from APITool
718
- (request as unknown as Record<string, unknown>).tools = convertToolsToOpenAIFormat(tools);
853
+ const openaiTools = convertToolsToOpenAIFormat(tools);
854
+ (request as unknown as Record<string, unknown>).tools = openaiTools;
855
+ // DEBUG: Log tools being sent
856
+ console.log(`[API] Sending ${tools.length} tools to ${apiFormat} API:`, JSON.stringify(openaiTools).substring(0, 500));
719
857
  } else {
720
858
  // Keep Anthropic format as-is
721
859
  request.tools = tools;
860
+ console.log(`[API] Sending ${tools.length} tools to ${apiFormat} API (Anthropic format)`);
722
861
  }
862
+ } else {
863
+ console.log(`[API] No tools being sent to API`);
723
864
  }
724
865
 
725
866
  const shouldUseExtendedThinking =
@@ -23,6 +23,7 @@ import type { InputEvent, NativeRendererType } from "../../../../../native/index
23
23
  import { spinnerFrames } from "../../shared/spinner-frames.js";
24
24
  import { MessageStoreImpl } from "./message-store.js";
25
25
  import { InputManagerImpl, KeyEvents, inputEventToNativeKeyEvent } from "./input-handler.js";
26
+ import { handleScrollEvent } from "./scroll-handler.js";
26
27
  import type {
27
28
  InteractiveRunnerProps,
28
29
  InteractiveState,
@@ -269,7 +270,34 @@ export class InteractiveRunner {
269
270
  return true;
270
271
  }
271
272
 
272
- // History navigation
273
+ // Chat history scroll - handle all scroll keys via scroll handler
274
+ // Debug: log event details
275
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
276
+ console.error("[InteractiveRunner] Key event:", {
277
+ code: event.code,
278
+ shift: event.shift,
279
+ ctrl: event.ctrl,
280
+ alt: event.alt,
281
+ is_special: event.is_special,
282
+ });
283
+ }
284
+
285
+ const scrollResult = handleScrollEvent(
286
+ event,
287
+ this.state.scrollOffset,
288
+ this.messageStore.messages.length
289
+ );
290
+
291
+ if (process.env.CODER_DEBUG_SCROLL === "1") {
292
+ console.error("[InteractiveRunner] Scroll result:", scrollResult);
293
+ }
294
+
295
+ if (scrollResult.handled) {
296
+ this.state = { ...this.state, scrollOffset: scrollResult.newOffset };
297
+ return true;
298
+ }
299
+
300
+ // History navigation (plain Up/Down without shift)
273
301
  if (KeyEvents.isUp(event)) {
274
302
  return this._handleHistoryUp();
275
303
  }
@@ -682,7 +710,7 @@ export class InteractiveRunner {
682
710
  content: `${s.messageCount} messages`,
683
711
  })) : [];
684
712
 
685
- // NativeRenderer expects camelCase field names (NAPI-RS converts snake_case to camelCase)
713
+ // NativeRenderer expects camelCase field names (NAPI-RS auto-converts snake_case)
686
714
  return {
687
715
  messages: renderMessages,
688
716
  inputValue: inputValue,
@@ -697,6 +725,7 @@ export class InteractiveRunner {
697
725
  searchQuery: "",
698
726
  searchResults: searchResults,
699
727
  searchSelected: 0,
728
+ scrollOffset: this.state.scrollOffset,
700
729
  };
701
730
  }
702
731