@ebowwa/coder 0.7.65 → 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.
@@ -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;
@@ -442,7 +551,22 @@ async function executeStreamAttempt(
442
551
  // OpenAI/Z.AI compatible format (for GLM-5, etc.)
443
552
  default: {
444
553
  if (event.choices && Array.isArray(event.choices)) {
445
- const choice = event.choices[0] as { delta?: { content?: string }; finish_reason?: string } | undefined;
554
+ const choice = event.choices[0] as {
555
+ delta?: {
556
+ content?: string;
557
+ tool_calls?: Array<{
558
+ id?: string;
559
+ index?: number;
560
+ function?: {
561
+ name?: string;
562
+ arguments?: string;
563
+ };
564
+ }>;
565
+ };
566
+ finish_reason?: string;
567
+ } | undefined;
568
+
569
+ // Handle text content
446
570
  if (choice?.delta?.content) {
447
571
  const text = choice.delta.content;
448
572
  if (currentTextBlock) {
@@ -456,11 +580,87 @@ async function executeStreamAttempt(
456
580
  firstToken = false;
457
581
  }
458
582
  }
583
+
584
+ // Handle tool calls (OpenAI format)
585
+ if (choice?.delta?.tool_calls && Array.isArray(choice.delta.tool_calls)) {
586
+ for (const toolCallDelta of choice.delta.tool_calls) {
587
+ const index = toolCallDelta.index ?? 0;
588
+ const toolCallId = toolCallDelta.id;
589
+
590
+ // Start a new tool call if we got an ID
591
+ if (toolCallId) {
592
+ // Finalize any existing tool use block at this index
593
+ if (currentToolUseBlock) {
594
+ try {
595
+ currentToolUseBlock.input = JSON.parse(toolUseInput);
596
+ } catch {
597
+ currentToolUseBlock.input = {};
598
+ }
599
+ currentContent.push(currentToolUseBlock);
600
+ callbacks.onToolUse?.({
601
+ id: currentToolUseBlock.id,
602
+ name: currentToolUseBlock.name,
603
+ input: currentToolUseBlock.input,
604
+ });
605
+ }
606
+
607
+ // Start new tool use block
608
+ currentToolUseBlock = {
609
+ type: "tool_use",
610
+ id: toolCallId,
611
+ name: toolCallDelta.function?.name || "",
612
+ input: {},
613
+ };
614
+ toolUseInput = "";
615
+
616
+ if (firstToken) {
617
+ ttft = Date.now() - startTime;
618
+ firstToken = false;
619
+ }
620
+ }
621
+
622
+ // Accumulate arguments for current tool call
623
+ if (toolCallDelta.function?.arguments && currentToolUseBlock) {
624
+ toolUseInput += toolCallDelta.function.arguments;
625
+ }
626
+ }
627
+ }
628
+
629
+ // Handle finish reason
459
630
  if (choice?.finish_reason) {
631
+ // Finalize any pending text block
460
632
  if (currentTextBlock) {
461
633
  currentContent.push(currentTextBlock);
462
634
  currentTextBlock = null;
463
635
  }
636
+
637
+ // Finalize any pending tool use block
638
+ if (currentToolUseBlock) {
639
+ try {
640
+ currentToolUseBlock.input = JSON.parse(toolUseInput);
641
+ } catch {
642
+ currentToolUseBlock.input = {};
643
+ }
644
+ currentContent.push(currentToolUseBlock);
645
+ callbacks.onToolUse?.({
646
+ id: currentToolUseBlock.id,
647
+ name: currentToolUseBlock.name,
648
+ input: currentToolUseBlock.input,
649
+ });
650
+ currentToolUseBlock = null;
651
+ toolUseInput = "";
652
+ }
653
+
654
+ // Map finish reasons
655
+ let stopReason: StopReason = "end_turn";
656
+ if (choice.finish_reason === "tool_calls" || choice.finish_reason === "function_call") {
657
+ stopReason = "tool_use";
658
+ } else if (choice.finish_reason === "length") {
659
+ stopReason = "max_tokens";
660
+ } else if (choice.finish_reason === "stop") {
661
+ stopReason = "end_turn";
662
+ }
663
+
464
664
  if (!message) {
465
665
  message = {
466
666
  id: `msg-${Date.now()}`,
@@ -468,12 +668,12 @@ async function executeStreamAttempt(
468
668
  role: "assistant",
469
669
  content: currentContent,
470
670
  model: model,
471
- stop_reason: (choice.finish_reason === "stop" ? "end_turn" : choice.finish_reason === "length" ? "max_tokens" : "end_turn") as StopReason,
671
+ stop_reason: stopReason,
472
672
  stop_sequence: null,
473
673
  usage: { input_tokens: 0, output_tokens: 0 },
474
674
  };
475
675
  } else {
476
- message.stop_reason = (choice.finish_reason === "stop" ? "end_turn" : choice.finish_reason === "length" ? "max_tokens" : "end_turn") as StopReason;
676
+ message.stop_reason = stopReason;
477
677
  }
478
678
  }
479
679
  }
@@ -556,30 +756,7 @@ export async function createMessageStream(
556
756
 
557
757
  const startTime = Date.now();
558
758
 
559
- // Build cached messages
560
- const cachedMessages = buildCachedMessages(messages, cacheConfig);
561
-
562
- // Build system prompt with cache control
563
- const cachedSystemPrompt = buildSystemPrompt(systemPrompt, cacheConfig);
564
-
565
- // Build request
566
- const request: APIRequest = {
567
- model,
568
- max_tokens: maxTokens,
569
- messages: cachedMessages.map((m) => ({
570
- role: m.role,
571
- content: m.content,
572
- })),
573
- stream: true,
574
- };
575
-
576
- if (cachedSystemPrompt) {
577
- request.system = cachedSystemPrompt;
578
- }
579
-
580
- // Tools will be set after determining API format (for format conversion)
581
-
582
- // Resolve provider based on model name
759
+ // Resolve provider FIRST to determine API format
583
760
  const providerInfo = resolveProvider(model);
584
761
 
585
762
  // Determine API endpoint and headers based on provider
@@ -619,6 +796,52 @@ export async function createMessageStream(
619
796
  };
620
797
  }
621
798
 
799
+ // Build cached messages
800
+ const cachedMessages = buildCachedMessages(messages, cacheConfig);
801
+
802
+ // Build system prompt with cache control
803
+ const cachedSystemPrompt = buildSystemPrompt(systemPrompt, cacheConfig);
804
+
805
+ // Build request with format-appropriate message conversion
806
+ let requestMessages: unknown;
807
+ if (apiFormat === "openai") {
808
+ // Convert to OpenAI format (handles tool_use and tool_result properly)
809
+ const openAIMessages = convertMessagesToOpenAIFormat(cachedMessages);
810
+
811
+ // Add system prompt as first message for OpenAI format
812
+ if (cachedSystemPrompt) {
813
+ const systemText = typeof cachedSystemPrompt === "string"
814
+ ? cachedSystemPrompt
815
+ : cachedSystemPrompt.map(b => b.text).join("\n\n");
816
+ requestMessages = [
817
+ { role: "system", content: systemText },
818
+ ...openAIMessages,
819
+ ];
820
+ } else {
821
+ requestMessages = openAIMessages;
822
+ }
823
+ } else {
824
+ // Keep Anthropic format
825
+ requestMessages = cachedMessages.map((m) => ({
826
+ role: m.role,
827
+ content: m.content,
828
+ }));
829
+ }
830
+
831
+ // Build request
832
+ const request: APIRequest = {
833
+ model,
834
+ max_tokens: maxTokens,
835
+ messages: requestMessages as Message[],
836
+ stream: true,
837
+ };
838
+
839
+ if (cachedSystemPrompt && apiFormat === "anthropic") {
840
+ // Anthropic format uses separate system field
841
+ // OpenAI format already has system as first message
842
+ request.system = cachedSystemPrompt;
843
+ }
844
+
622
845
  // Set tools with format conversion if needed
623
846
  if (tools && tools.length > 0) {
624
847
  if (apiFormat === "openai") {
@@ -200,16 +200,25 @@ export const KeyEvents = {
200
200
  return event.code === "down" || event.code === "Down";
201
201
  },
202
202
 
203
+ /** Check if event is Space key */
204
+ isSpace(event: NativeKeyEvent): boolean {
205
+ return event.code === " " || event.code === "Space";
206
+ },
207
+
203
208
  /** Check if event is a printable character */
204
209
  isPrintable(event: NativeKeyEvent): boolean {
205
210
  if (event.is_special) return false;
206
211
  const code = event.code;
212
+ // Space is printable (normalized to "Space" but should work in text input)
213
+ if (code === " " || code === "Space") return true;
207
214
  // Single character that is not a control character
208
215
  return code.length === 1 && !event.ctrl;
209
216
  },
210
217
 
211
218
  /** Get the character from the event */
212
219
  getChar(event: NativeKeyEvent): string {
220
+ // Handle normalized space
221
+ if (event.code === "Space") return " ";
213
222
  return event.code;
214
223
  },
215
224
  };
@@ -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,21 +710,22 @@ 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 snake_case field names (NAPI-RS keeps Rust naming)
686
714
  return {
687
715
  messages: renderMessages,
688
- inputValue: inputValue,
689
- cursorPos: cursorPos,
690
- statusText: statusText,
691
- isLoading: isLoading,
692
- streamingText: streamingText,
716
+ input_value: inputValue,
717
+ cursor_pos: cursorPos,
718
+ status_text: statusText,
719
+ is_loading: isLoading,
720
+ streaming_text: streamingText,
693
721
  model: this.props.model,
694
- showHelp: helpMode,
695
- helpText: helpText,
696
- searchMode: sessionSelectMode,
697
- searchQuery: "",
698
- searchResults: searchResults,
699
- searchSelected: 0,
722
+ show_help: helpMode,
723
+ help_text: helpText,
724
+ search_mode: sessionSelectMode,
725
+ search_query: "",
726
+ search_results: searchResults,
727
+ search_selected: 0,
728
+ scroll_offset: this.state.scrollOffset,
700
729
  };
701
730
  }
702
731