@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.
- package/dist/index.js +279 -34
- package/dist/index.js.map +156 -0
- package/dist/interfaces/ui/terminal/cli/bootstrap.js.map +141 -0
- package/dist/interfaces/ui/terminal/cli/index.js +272 -27
- package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
- package/dist/native/index.darwin-arm64.node +0 -0
- package/native/index.darwin-arm64.node +0 -0
- package/package.json +2 -2
- package/packages/src/core/api-client-impl.ts +250 -27
- package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +9 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +42 -13
- package/packages/src/interfaces/ui/terminal/cli/interactive/scroll-handler.ts +284 -0
- package/packages/src/native/index.ts +1 -0
|
@@ -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 {
|
|
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:
|
|
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 =
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
713
|
+
// NativeRenderer expects snake_case field names (NAPI-RS keeps Rust naming)
|
|
686
714
|
return {
|
|
687
715
|
messages: renderMessages,
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
|