@bubblebrain-ai/bubble 0.0.24 → 0.0.26
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/README.md +5 -3
- package/dist/agent.js +1 -1
- package/dist/clipboard.d.ts +14 -0
- package/dist/clipboard.js +132 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +22 -6
- package/dist/goal/format.js +34 -4
- package/dist/goal/store.d.ts +3 -0
- package/dist/goal/store.js +14 -1
- package/dist/goal/usage.d.ts +2 -0
- package/dist/goal/usage.js +3 -0
- package/dist/main.js +23 -42
- package/dist/model-catalog.d.ts +3 -1
- package/dist/model-catalog.js +17 -28
- package/dist/prompt/compose.js +1 -1
- package/dist/provider-anthropic.d.ts +4 -0
- package/dist/provider-anthropic.js +31 -0
- package/dist/provider-ark-responses.d.ts +17 -0
- package/dist/provider-ark-responses.js +462 -0
- package/dist/provider-transform.js +7 -0
- package/dist/provider.d.ts +7 -0
- package/dist/provider.js +170 -27
- package/dist/slash-commands/commands.js +22 -0
- package/dist/tools/todo.js +22 -38
- package/dist/tui/detect-theme.d.ts +1 -0
- package/dist/tui/detect-theme.js +23 -0
- package/dist/tui/image-display.d.ts +13 -0
- package/dist/tui/image-display.js +49 -0
- package/dist/tui/input-history.d.ts +37 -6
- package/dist/tui/input-history.js +194 -23
- package/dist/tui/model-switch.d.ts +42 -0
- package/dist/tui/model-switch.js +55 -0
- package/dist/tui-ink/app.d.ts +32 -2
- package/dist/tui-ink/app.js +1409 -549
- package/dist/tui-ink/approval/select.js +10 -0
- package/dist/tui-ink/detect-theme.d.ts +1 -2
- package/dist/tui-ink/detect-theme.js +1 -87
- package/dist/tui-ink/display-history.d.ts +1 -0
- package/dist/tui-ink/display-history.js +11 -0
- package/dist/tui-ink/feedback-dialog.js +10 -0
- package/dist/tui-ink/feishu-setup-picker.js +10 -0
- package/dist/tui-ink/footer.d.ts +1 -0
- package/dist/tui-ink/footer.js +8 -2
- package/dist/tui-ink/input-box.d.ts +71 -9
- package/dist/tui-ink/input-box.js +359 -121
- package/dist/tui-ink/input-history.d.ts +1 -16
- package/dist/tui-ink/input-history.js +1 -79
- package/dist/tui-ink/input-queue.d.ts +12 -0
- package/dist/tui-ink/input-queue.js +17 -0
- package/dist/tui-ink/key-events.d.ts +9 -0
- package/dist/tui-ink/key-events.js +8 -0
- package/dist/tui-ink/markdown.js +1 -1
- package/dist/tui-ink/message-list.d.ts +19 -1
- package/dist/tui-ink/message-list.js +111 -32
- package/dist/tui-ink/model-picker.d.ts +25 -2
- package/dist/tui-ink/model-picker.js +237 -20
- package/dist/tui-ink/plan-confirm.js +10 -0
- package/dist/tui-ink/question-dialog.js +46 -10
- package/dist/tui-ink/run.d.ts +10 -1
- package/dist/tui-ink/run.js +27 -42
- package/dist/tui-ink/session-picker.js +3 -0
- package/dist/tui-ink/submit-dedupe.d.ts +5 -0
- package/dist/tui-ink/submit-dedupe.js +25 -0
- package/dist/tui-ink/terminal-mouse.d.ts +24 -1
- package/dist/tui-ink/terminal-mouse.js +76 -21
- package/dist/tui-ink/theme.d.ts +6 -3
- package/dist/tui-ink/theme.js +10 -4
- package/dist/tui-ink/welcome.d.ts +1 -0
- package/dist/tui-ink/welcome.js +34 -27
- package/dist/variant/variant-resolver.js +4 -1
- package/package.json +1 -5
- package/dist/tui/clipboard.d.ts +0 -1
- package/dist/tui/clipboard.js +0 -53
- package/dist/tui/escape-confirmation.d.ts +0 -15
- package/dist/tui/escape-confirmation.js +0 -30
- package/dist/tui/global-key-router.d.ts +0 -3
- package/dist/tui/global-key-router.js +0 -87
- package/dist/tui/markdown-inline.d.ts +0 -22
- package/dist/tui/markdown-inline.js +0 -68
- package/dist/tui/markdown-theme-rules.d.ts +0 -23
- package/dist/tui/markdown-theme-rules.js +0 -164
- package/dist/tui/markdown-theme.d.ts +0 -5
- package/dist/tui/markdown-theme.js +0 -27
- package/dist/tui/opencode-spinner.d.ts +0 -22
- package/dist/tui/opencode-spinner.js +0 -216
- package/dist/tui/prompt-keybindings.d.ts +0 -42
- package/dist/tui/prompt-keybindings.js +0 -35
- package/dist/tui/render-signature.d.ts +0 -1
- package/dist/tui/render-signature.js +0 -7
- package/dist/tui/run.d.ts +0 -67
- package/dist/tui/run.js +0 -10166
- package/dist/tui/sidebar-mcp.d.ts +0 -31
- package/dist/tui/sidebar-mcp.js +0 -62
- package/dist/tui/sidebar-state.d.ts +0 -12
- package/dist/tui/sidebar-state.js +0 -69
- package/dist/tui/streaming-tool-args.d.ts +0 -15
- package/dist/tui/streaming-tool-args.js +0 -30
- package/dist/tui/tool-renderers/fallback.d.ts +0 -2
- package/dist/tui/tool-renderers/fallback.js +0 -75
- package/dist/tui/tool-renderers/registry.d.ts +0 -3
- package/dist/tui/tool-renderers/registry.js +0 -11
- package/dist/tui/tool-renderers/subagent.d.ts +0 -2
- package/dist/tui/tool-renderers/subagent.js +0 -135
- package/dist/tui/tool-renderers/types.d.ts +0 -36
- package/dist/tui/tool-renderers/types.js +0 -1
- package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
- package/dist/tui/tool-renderers/write-preview.js +0 -32
- package/dist/tui/tool-renderers/write.d.ts +0 -6
- package/dist/tui/tool-renderers/write.js +0 -88
- package/dist/tui/transcript-scroll.d.ts +0 -25
- package/dist/tui/transcript-scroll.js +0 -20
- package/dist/tui-ink/transcript-viewport-math.d.ts +0 -11
- package/dist/tui-ink/transcript-viewport-math.js +0 -17
- package/dist/tui-ink/transcript-viewport.d.ts +0 -24
- package/dist/tui-ink/transcript-viewport.js +0 -83
- package/dist/tui-opentui/app.d.ts +0 -54
- package/dist/tui-opentui/app.js +0 -1371
- package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
- package/dist/tui-opentui/approval/approval-dialog.js +0 -155
- package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
- package/dist/tui-opentui/approval/diff-view.js +0 -43
- package/dist/tui-opentui/approval/select.d.ts +0 -37
- package/dist/tui-opentui/approval/select.js +0 -91
- package/dist/tui-opentui/detect-theme.d.ts +0 -2
- package/dist/tui-opentui/detect-theme.js +0 -87
- package/dist/tui-opentui/display-history.d.ts +0 -56
- package/dist/tui-opentui/display-history.js +0 -130
- package/dist/tui-opentui/edit-diff.d.ts +0 -11
- package/dist/tui-opentui/edit-diff.js +0 -57
- package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
- package/dist/tui-opentui/feedback-dialog.js +0 -164
- package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
- package/dist/tui-opentui/feishu-setup-picker.js +0 -272
- package/dist/tui-opentui/file-mentions.d.ts +0 -29
- package/dist/tui-opentui/file-mentions.js +0 -174
- package/dist/tui-opentui/footer.d.ts +0 -26
- package/dist/tui-opentui/footer.js +0 -40
- package/dist/tui-opentui/image-paste.d.ts +0 -54
- package/dist/tui-opentui/image-paste.js +0 -288
- package/dist/tui-opentui/input-box.d.ts +0 -32
- package/dist/tui-opentui/input-box.js +0 -462
- package/dist/tui-opentui/input-history.d.ts +0 -16
- package/dist/tui-opentui/input-history.js +0 -79
- package/dist/tui-opentui/markdown.d.ts +0 -66
- package/dist/tui-opentui/markdown.js +0 -127
- package/dist/tui-opentui/message-list.d.ts +0 -31
- package/dist/tui-opentui/message-list.js +0 -131
- package/dist/tui-opentui/model-picker.d.ts +0 -63
- package/dist/tui-opentui/model-picker.js +0 -450
- package/dist/tui-opentui/plan-confirm.d.ts +0 -9
- package/dist/tui-opentui/plan-confirm.js +0 -124
- package/dist/tui-opentui/question-dialog.d.ts +0 -10
- package/dist/tui-opentui/question-dialog.js +0 -110
- package/dist/tui-opentui/recent-activity.d.ts +0 -8
- package/dist/tui-opentui/recent-activity.js +0 -71
- package/dist/tui-opentui/run-session-picker.d.ts +0 -10
- package/dist/tui-opentui/run-session-picker.js +0 -28
- package/dist/tui-opentui/run.d.ts +0 -38
- package/dist/tui-opentui/run.js +0 -48
- package/dist/tui-opentui/session-picker.d.ts +0 -12
- package/dist/tui-opentui/session-picker.js +0 -120
- package/dist/tui-opentui/theme.d.ts +0 -89
- package/dist/tui-opentui/theme.js +0 -157
- package/dist/tui-opentui/todos.d.ts +0 -9
- package/dist/tui-opentui/todos.js +0 -45
- package/dist/tui-opentui/trace-groups.d.ts +0 -27
- package/dist/tui-opentui/trace-groups.js +0 -455
- package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
- package/dist/tui-opentui/use-terminal-size.js +0 -5
- package/dist/tui-opentui/welcome.d.ts +0 -25
- package/dist/tui-opentui/welcome.js +0 -77
package/dist/provider.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import OpenAI from "openai";
|
|
7
7
|
import { appendFileSync } from "node:fs";
|
|
8
8
|
import { createAnthropicMessagesProvider } from "./provider-anthropic.js";
|
|
9
|
+
import { createArkResponsesProvider } from "./provider-ark-responses.js";
|
|
9
10
|
import { createOpenAICodexProvider, isOpenAICodexBaseUrl } from "./provider-openai-codex.js";
|
|
10
11
|
import { createProviderProtocolArtifactFilter } from "./provider-artifacts.js";
|
|
11
12
|
import { resolveProviderRequestConfig } from "./provider-transform.js";
|
|
@@ -76,9 +77,13 @@ export function createUnavailableProvider(message) {
|
|
|
76
77
|
return { streamChat, complete };
|
|
77
78
|
}
|
|
78
79
|
export function createProviderInstance(options) {
|
|
79
|
-
|
|
80
|
+
const protocol = resolveProviderProtocol(options);
|
|
81
|
+
if (protocol === "anthropic-messages") {
|
|
80
82
|
return createAnthropicMessagesProvider(options);
|
|
81
83
|
}
|
|
84
|
+
if (protocol === "ark-responses") {
|
|
85
|
+
return createArkResponsesProvider(options);
|
|
86
|
+
}
|
|
82
87
|
if (isOpenAICodexBaseUrl(options.baseURL)) {
|
|
83
88
|
return createOpenAICodexProvider({
|
|
84
89
|
...options,
|
|
@@ -110,8 +115,10 @@ export function createProviderInstance(options) {
|
|
|
110
115
|
tool_choice: tools && tools.length > 0 ? chatOptions.toolChoice ?? "auto" : undefined,
|
|
111
116
|
stream: true,
|
|
112
117
|
};
|
|
113
|
-
//
|
|
114
|
-
|
|
118
|
+
// Several OpenAI-compatible streaming APIs only emit final usage when this
|
|
119
|
+
// flag is set. Without it, downstream goal/stat accounting can only report
|
|
120
|
+
// "usage unavailable".
|
|
121
|
+
if (shouldRequestStreamUsage(options)) {
|
|
115
122
|
body.stream_options = { include_usage: true };
|
|
116
123
|
}
|
|
117
124
|
if (!requestConfig.omitTemperature) {
|
|
@@ -129,28 +136,39 @@ export function createProviderInstance(options) {
|
|
|
129
136
|
if (requestConfig.reasoningEffort && requestConfig.reasoningEffort !== "off") {
|
|
130
137
|
body.reasoning = { enabled: true };
|
|
131
138
|
}
|
|
139
|
+
const createCompletion = async (requestBody) => {
|
|
140
|
+
try {
|
|
141
|
+
return await client.chat.completions.create(requestBody, {
|
|
142
|
+
signal: chatOptions.abortSignal,
|
|
143
|
+
...(chatOptions.rateLimitPolicy === "defer" ? { maxRetries: 0 } : {}),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error?.status === 429) {
|
|
148
|
+
const retryAfterHeader = error?.headers?.["retry-after"];
|
|
149
|
+
const retryAfterSeconds = Number(retryAfterHeader);
|
|
150
|
+
throw new RateLimitError(error?.message || "Rate limited (429)", {
|
|
151
|
+
status: 429,
|
|
152
|
+
retryAfterMs: Number.isFinite(retryAfterSeconds) ? Math.round(retryAfterSeconds * 1000) : undefined,
|
|
153
|
+
cause: error,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
if (shouldUseNonStreamingToolCalls(options, tools, chatOptions.toolChoice)) {
|
|
160
|
+
body.stream = false;
|
|
161
|
+
delete body.stream_options;
|
|
162
|
+
const response = await createCompletion(body);
|
|
163
|
+
yield* translateOpenAIFullResponse(response);
|
|
164
|
+
yield { type: "done" };
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
132
167
|
// Rate-limit contract (design §4.5): "defer" disables the SDK's own
|
|
133
168
|
// retries so the caller is the single 429 backoff layer; either policy
|
|
134
169
|
// surfaces a final 429 as a typed RateLimitError instead of a string.
|
|
135
170
|
let stream;
|
|
136
|
-
|
|
137
|
-
stream = (await client.chat.completions.create(body, {
|
|
138
|
-
signal: chatOptions.abortSignal,
|
|
139
|
-
...(chatOptions.rateLimitPolicy === "defer" ? { maxRetries: 0 } : {}),
|
|
140
|
-
}));
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
if (error?.status === 429) {
|
|
144
|
-
const retryAfterHeader = error?.headers?.["retry-after"];
|
|
145
|
-
const retryAfterSeconds = Number(retryAfterHeader);
|
|
146
|
-
throw new RateLimitError(error?.message || "Rate limited (429)", {
|
|
147
|
-
status: 429,
|
|
148
|
-
retryAfterMs: Number.isFinite(retryAfterSeconds) ? Math.round(retryAfterSeconds * 1000) : undefined,
|
|
149
|
-
cause: error,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
throw error;
|
|
153
|
-
}
|
|
171
|
+
stream = (await createCompletion(body));
|
|
154
172
|
yield* translateOpenAIStream(stream, {
|
|
155
173
|
toolArgsMergeMode: resolveToolArgsMergeMode(options.providerId || "", options.baseURL),
|
|
156
174
|
reasoningMergeMode: resolveReasoningMergeMode(options.providerId || "", options.baseURL),
|
|
@@ -192,6 +210,10 @@ function resolveProviderProtocol(options) {
|
|
|
192
210
|
return options.protocol;
|
|
193
211
|
const providerId = (options.providerId || "").toLowerCase();
|
|
194
212
|
const baseURL = options.baseURL.toLowerCase();
|
|
213
|
+
if (providerId === "doubao"
|
|
214
|
+
&& baseURL.replace(/\/+$/, "") === "https://ark.cn-beijing.volces.com/api/v3") {
|
|
215
|
+
return "ark-responses";
|
|
216
|
+
}
|
|
195
217
|
if (providerId === "anthropic"
|
|
196
218
|
|| providerId.endsWith("-anthropic")
|
|
197
219
|
|| baseURL.includes("/anthropic")) {
|
|
@@ -207,6 +229,24 @@ function isMiniMaxOpenAICompatible(options) {
|
|
|
207
229
|
|| baseURL.includes("api.minimaxi.com/v1")
|
|
208
230
|
|| baseURL.includes("api.minimax.io/v1");
|
|
209
231
|
}
|
|
232
|
+
function shouldRequestStreamUsage(options) {
|
|
233
|
+
const providerId = (options.providerId || "").toLowerCase();
|
|
234
|
+
return providerId === "openai"
|
|
235
|
+
|| providerId === "deepseek"
|
|
236
|
+
|| providerId === "moonshot-cn"
|
|
237
|
+
|| providerId === "moonshot-intl"
|
|
238
|
+
|| providerId === "zhipuai"
|
|
239
|
+
|| providerId === "zhipuai-coding-plan"
|
|
240
|
+
|| providerId === "zai"
|
|
241
|
+
|| providerId === "zai-coding-plan"
|
|
242
|
+
|| isMiniMaxOpenAICompatible(options);
|
|
243
|
+
}
|
|
244
|
+
function shouldUseNonStreamingToolCalls(options, tools, toolChoice) {
|
|
245
|
+
return (options.providerId || "").toLowerCase() === "doubao"
|
|
246
|
+
&& !!tools
|
|
247
|
+
&& tools.length > 0
|
|
248
|
+
&& toolChoice !== "none";
|
|
249
|
+
}
|
|
210
250
|
export function normalizeToolArgsDetailed(raw) {
|
|
211
251
|
const s = (raw ?? "").trim();
|
|
212
252
|
if (!s) {
|
|
@@ -307,6 +347,101 @@ function extractBalancedJson(s, start) {
|
|
|
307
347
|
}
|
|
308
348
|
return null;
|
|
309
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Convert a non-streaming OpenAI-compatible chat-completions response into the
|
|
352
|
+
* same chunk protocol used by the streaming adapter. This is used for provider
|
|
353
|
+
* tool-call paths where streamed function arguments are not reliable enough to
|
|
354
|
+
* execute safely.
|
|
355
|
+
*/
|
|
356
|
+
export async function* translateOpenAIFullResponse(response) {
|
|
357
|
+
const usageChunk = usageToStreamChunk(response?.usage);
|
|
358
|
+
if (usageChunk)
|
|
359
|
+
yield usageChunk;
|
|
360
|
+
const choice = response?.choices?.[0];
|
|
361
|
+
const finishReason = choice?.finish_reason;
|
|
362
|
+
const truncatedByLength = finishReason === "length";
|
|
363
|
+
const message = choice?.message;
|
|
364
|
+
if (!message)
|
|
365
|
+
return;
|
|
366
|
+
const reasoningDetails = extractReasoningDetailsText(message.reasoning_details);
|
|
367
|
+
const reasoning = reasoningDetails
|
|
368
|
+
?? (typeof message.reasoning === "string" ? message.reasoning : undefined)
|
|
369
|
+
?? (typeof message.thinking === "string" ? message.thinking : undefined)
|
|
370
|
+
?? (typeof message.reasoning_content === "string" ? message.reasoning_content : undefined);
|
|
371
|
+
if (reasoning) {
|
|
372
|
+
yield { type: "reasoning_delta", content: reasoning };
|
|
373
|
+
}
|
|
374
|
+
if (typeof message.content === "string" && message.content) {
|
|
375
|
+
const textFilter = createProviderProtocolArtifactFilter();
|
|
376
|
+
const cleaned = textFilter.push(message.content) + textFilter.flush();
|
|
377
|
+
if (cleaned) {
|
|
378
|
+
yield { type: "text", content: cleaned };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
382
|
+
for (let index = 0; index < toolCalls.length; index += 1) {
|
|
383
|
+
const toolCall = toolCalls[index];
|
|
384
|
+
const name = typeof toolCall?.function?.name === "string" ? toolCall.function.name : "";
|
|
385
|
+
if (!name)
|
|
386
|
+
continue;
|
|
387
|
+
const id = typeof toolCall?.id === "string" && toolCall.id
|
|
388
|
+
? toolCall.id
|
|
389
|
+
: `call_${index}`;
|
|
390
|
+
const rawArgs = typeof toolCall?.function?.arguments === "string"
|
|
391
|
+
? toolCall.function.arguments
|
|
392
|
+
: JSON.stringify(toolCall?.function?.arguments ?? {});
|
|
393
|
+
const normalized = normalizeToolArgsDetailed(rawArgs);
|
|
394
|
+
const corrupt = normalized.corrupt || truncatedByLength;
|
|
395
|
+
debugToolArgs({
|
|
396
|
+
stage: "full-response-tool-call",
|
|
397
|
+
id,
|
|
398
|
+
name,
|
|
399
|
+
entryArgs: rawArgs,
|
|
400
|
+
finalArgs: normalized.args,
|
|
401
|
+
finishReason,
|
|
402
|
+
corrupt,
|
|
403
|
+
});
|
|
404
|
+
yield { type: "tool_call", id, name, arguments: "", isStart: true, isEnd: false };
|
|
405
|
+
if (rawArgs) {
|
|
406
|
+
yield { type: "tool_call", id, name, arguments: rawArgs, isStart: false, isEnd: false };
|
|
407
|
+
}
|
|
408
|
+
yield {
|
|
409
|
+
type: "tool_call",
|
|
410
|
+
id,
|
|
411
|
+
name,
|
|
412
|
+
arguments: "",
|
|
413
|
+
argumentsFull: normalized.args,
|
|
414
|
+
argumentsCorrupt: corrupt || undefined,
|
|
415
|
+
isStart: false,
|
|
416
|
+
isEnd: true,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function usageToStreamChunk(usage) {
|
|
421
|
+
if (!usage)
|
|
422
|
+
return undefined;
|
|
423
|
+
return {
|
|
424
|
+
type: "usage",
|
|
425
|
+
usage: {
|
|
426
|
+
promptTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0,
|
|
427
|
+
completionTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0,
|
|
428
|
+
promptCacheHitTokens: typeof usage.prompt_cache_hit_tokens === "number"
|
|
429
|
+
? usage.prompt_cache_hit_tokens
|
|
430
|
+
: typeof usage.prompt_tokens_details?.cached_tokens === "number"
|
|
431
|
+
? usage.prompt_tokens_details.cached_tokens
|
|
432
|
+
: undefined,
|
|
433
|
+
promptCacheMissTokens: typeof usage.prompt_cache_miss_tokens === "number"
|
|
434
|
+
? usage.prompt_cache_miss_tokens
|
|
435
|
+
: typeof usage.prompt_tokens_details?.cached_tokens === "number" && typeof usage.prompt_tokens === "number"
|
|
436
|
+
? Math.max(0, usage.prompt_tokens - usage.prompt_tokens_details.cached_tokens)
|
|
437
|
+
: undefined,
|
|
438
|
+
reasoningTokens: typeof usage.completion_tokens_details?.reasoning_tokens === "number"
|
|
439
|
+
? usage.completion_tokens_details.reasoning_tokens
|
|
440
|
+
: undefined,
|
|
441
|
+
totalTokens: typeof usage.total_tokens === "number" ? usage.total_tokens : undefined,
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|
|
310
445
|
/**
|
|
311
446
|
* Convert an OpenAI-compatible chat-completions stream into our internal StreamChunk events.
|
|
312
447
|
*
|
|
@@ -345,14 +480,15 @@ export async function* translateOpenAIStream(stream, options = {}) {
|
|
|
345
480
|
}
|
|
346
481
|
}
|
|
347
482
|
const normalized = normalizeToolArgsDetailed(entry.args);
|
|
348
|
-
|
|
483
|
+
const corrupt = normalized.corrupt || !!entry.corrupt;
|
|
484
|
+
debugToolArgs({ stage: "flush-end", id: entry.id, name: entry.name, entryArgs: entry.args, finalArgs: normalized.args, corrupt });
|
|
349
485
|
yield {
|
|
350
486
|
type: "tool_call",
|
|
351
487
|
id: entry.id,
|
|
352
488
|
name: entry.name,
|
|
353
489
|
arguments: "",
|
|
354
490
|
argumentsFull: normalized.args,
|
|
355
|
-
argumentsCorrupt:
|
|
491
|
+
argumentsCorrupt: corrupt || undefined,
|
|
356
492
|
isStart: false,
|
|
357
493
|
isEnd: true,
|
|
358
494
|
};
|
|
@@ -370,9 +506,10 @@ export async function* translateOpenAIStream(stream, options = {}) {
|
|
|
370
506
|
}
|
|
371
507
|
for await (const chunk of stream) {
|
|
372
508
|
rawChunkSeq += 1;
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
const
|
|
509
|
+
const choice = chunk.choices?.[0];
|
|
510
|
+
const delta = choice?.delta;
|
|
511
|
+
const usage = chunk.usage ?? choice?.usage;
|
|
512
|
+
const finishReason = choice?.finish_reason;
|
|
376
513
|
debugReasoningStream({
|
|
377
514
|
stage: "provider_raw",
|
|
378
515
|
providerId: options.debugProviderId,
|
|
@@ -511,7 +648,13 @@ export async function* translateOpenAIStream(stream, options = {}) {
|
|
|
511
648
|
}
|
|
512
649
|
}
|
|
513
650
|
}
|
|
514
|
-
if (finishReason === "
|
|
651
|
+
if (finishReason === "length") {
|
|
652
|
+
for (const entry of toolCalls.values()) {
|
|
653
|
+
entry.corrupt = true;
|
|
654
|
+
}
|
|
655
|
+
yield* flushToolCalls();
|
|
656
|
+
}
|
|
657
|
+
else if (finishReason === "tool_calls") {
|
|
515
658
|
yield* flushToolCalls();
|
|
516
659
|
}
|
|
517
660
|
}
|
|
@@ -8,6 +8,7 @@ import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingL
|
|
|
8
8
|
import { SessionManager } from "../session.js";
|
|
9
9
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
10
10
|
import { normalizeSingleLine } from "../text-display.js";
|
|
11
|
+
import { copyToClipboard } from "../clipboard.js";
|
|
11
12
|
import { formatRelativeTime } from "../tui/recent-activity.js";
|
|
12
13
|
import { HOOK_EVENT_NAMES, isHookEventName } from "../hooks/index.js";
|
|
13
14
|
import { isThinkingLevel } from "../variant/thinking-level.js";
|
|
@@ -402,6 +403,27 @@ const builtinSlashCommandEntries = [
|
|
|
402
403
|
ctx.clearMessages();
|
|
403
404
|
},
|
|
404
405
|
},
|
|
406
|
+
{
|
|
407
|
+
name: "copy",
|
|
408
|
+
description: "Copy the last assistant message to the system clipboard",
|
|
409
|
+
async handler(args, ctx) {
|
|
410
|
+
const lastAssistant = [...ctx.agent.messages]
|
|
411
|
+
.reverse()
|
|
412
|
+
.find((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim().length > 0);
|
|
413
|
+
if (!lastAssistant || typeof lastAssistant.content !== "string") {
|
|
414
|
+
return "No assistant message to copy yet.";
|
|
415
|
+
}
|
|
416
|
+
const text = lastAssistant.content;
|
|
417
|
+
try {
|
|
418
|
+
await copyToClipboard(text);
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
return `Failed to copy to clipboard: ${err?.message || String(err)}`;
|
|
422
|
+
}
|
|
423
|
+
const chars = text.length;
|
|
424
|
+
return `Copied last assistant message to clipboard (${chars} character${chars === 1 ? "" : "s"}).`;
|
|
425
|
+
},
|
|
426
|
+
},
|
|
405
427
|
{
|
|
406
428
|
name: "rewind",
|
|
407
429
|
description: "Rewind conversation and/or file edits to before an earlier message. Usage: /rewind [n] [--code|--chat]",
|
package/dist/tools/todo.js
CHANGED
|
@@ -13,50 +13,33 @@ export function createTodoTool(store) {
|
|
|
13
13
|
|
|
14
14
|
## When to use
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
1. Complex multi-step work — 3 or more distinct steps or file locations
|
|
18
|
-
2. Non-trivial tasks requiring planning or coordination across multiple operations
|
|
19
|
-
3. The user explicitly asks for a todo list
|
|
20
|
-
4. The user provides a list of things to do (numbered, comma-separated, bulleted)
|
|
21
|
-
5. New instructions arrive mid-session — capture them as todos before starting
|
|
22
|
-
6. Starting work on a task — mark it in_progress BEFORE beginning. Only one item may be in_progress at a time
|
|
23
|
-
7. Finishing a task — mark it completed immediately, don't batch completions
|
|
16
|
+
Default to just doing the work. Reach for a list only when actively tracking progress would genuinely help you or the user follow it — never to pad simple work with filler steps or to state the obvious. When in doubt, skip the list and do the task; a list you never meaningfully update is just noise.
|
|
24
17
|
|
|
25
|
-
|
|
18
|
+
A list earns its place when:
|
|
19
|
+
- The task is non-trivial and spans many actions across several areas of the codebase
|
|
20
|
+
- There are non-obvious phases or dependencies you must hold in mind to avoid losing track (a plain read → edit → test sequence does not count)
|
|
21
|
+
- The work is ambiguous and benefits from outlining the goals up front
|
|
22
|
+
- The user asked for several distinct things in one prompt, or gave a numbered/bulleted list
|
|
23
|
+
- The user explicitly asked for a todo list (aka TODOs)
|
|
24
|
+
- You discover extra steps mid-task and intend to finish them before yielding
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
1. There is a single, straightforward task
|
|
29
|
-
2. The task is trivial and tracking provides no organizational benefit
|
|
30
|
-
3. The work can be completed in fewer than 3 trivial steps
|
|
31
|
-
4. The request is purely conversational or informational
|
|
26
|
+
## Quality bar
|
|
32
27
|
|
|
33
|
-
If
|
|
28
|
+
If you do make a list, make a good one: meaningful, logically ordered steps that are easy to verify as you go.
|
|
34
29
|
|
|
35
|
-
|
|
30
|
+
Good — distinct, verifiable steps for genuinely multi-part work:
|
|
31
|
+
1. Add CSS variables for the color palette
|
|
32
|
+
2. Add the toggle with localStorage state
|
|
33
|
+
3. Refactor components to use the variables
|
|
34
|
+
4. Verify every view for readability
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Assistant: *creates a 5-item todo: toggle UI, theme state, CSS tokens, update components, run tests + build*
|
|
40
|
-
<reasoning>Multiple distinct steps across UI, state, styles, and verification. User explicitly asked for tests + build.</reasoning>
|
|
41
|
-
</example>
|
|
36
|
+
Good — scope a search uncovers makes the list worth it:
|
|
37
|
+
"Rename getCwd across the project" → grep finds 15 call sites in 8 files → one item per file so none are missed.
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
Assistant: *greps, finds 15 call sites across 8 files, creates a per-file todo list*
|
|
46
|
-
<reasoning>Scope discovered via grep shows many locations; a todo ensures each file is tracked and none are missed.</reasoning>
|
|
47
|
-
</example>
|
|
39
|
+
Bad — padding a task you could just do; do NOT create a list for this:
|
|
40
|
+
"Fix the typo in the README title" → 1. Find typo 2. Open file 3. Fix it 4. Save. That is one edit — just make it.
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
User: How do I print "Hello World" in Python?
|
|
51
|
-
Assistant: *answers in one sentence with a snippet — no todo*
|
|
52
|
-
<reasoning>Informational, one-step, no tracking benefit.</reasoning>
|
|
53
|
-
</example>
|
|
54
|
-
|
|
55
|
-
<example>
|
|
56
|
-
User: Add a comment to calculateTotal explaining what it does.
|
|
57
|
-
Assistant: *calls edit directly — no todo*
|
|
58
|
-
<reasoning>Single, localized change in one file.</reasoning>
|
|
59
|
-
</example>
|
|
42
|
+
Bad — vague, unverifiable filler: "Make it work", "Improve the styling", "Clean things up".
|
|
60
43
|
|
|
61
44
|
## Task states
|
|
62
45
|
|
|
@@ -73,7 +56,8 @@ Each item needs:
|
|
|
73
56
|
- Update status in real time; mark completed IMMEDIATELY on finishing.
|
|
74
57
|
- Never mark completed if tests are failing, implementation is partial, errors are unresolved, or needed files are missing — keep as in_progress.
|
|
75
58
|
- When blocked, add a new task describing what must be resolved.
|
|
76
|
-
- Remove items that are no longer relevant; don't leave stale entries
|
|
59
|
+
- Remove items that are no longer relevant; don't leave stale entries.
|
|
60
|
+
- Do not re-send the list when nothing meaningful has changed since the last call; update only after real progress.`,
|
|
77
61
|
parameters: {
|
|
78
62
|
type: "object",
|
|
79
63
|
properties: {
|
package/dist/tui/detect-theme.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
export async function detectTerminalTheme(timeoutMs = 150) {
|
|
2
3
|
const fromEnv = parseColorFgBg(process.env.COLORFGBG);
|
|
3
4
|
if (fromEnv)
|
|
@@ -7,6 +8,9 @@ export async function detectTerminalTheme(timeoutMs = 150) {
|
|
|
7
8
|
if (fromOsc)
|
|
8
9
|
return fromOsc;
|
|
9
10
|
}
|
|
11
|
+
const fromOs = detectOsAppearanceTheme();
|
|
12
|
+
if (fromOs)
|
|
13
|
+
return fromOs;
|
|
10
14
|
return "dark";
|
|
11
15
|
}
|
|
12
16
|
function parseColorFgBg(value) {
|
|
@@ -85,3 +89,22 @@ function relativeLuminance(r, g, b) {
|
|
|
85
89
|
const channel = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
86
90
|
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
87
91
|
}
|
|
92
|
+
function detectOsAppearanceTheme() {
|
|
93
|
+
if (process.platform !== "darwin")
|
|
94
|
+
return null;
|
|
95
|
+
try {
|
|
96
|
+
const output = execFileSync("/usr/bin/defaults", ["read", "-g", "AppleInterfaceStyle"], {
|
|
97
|
+
encoding: "utf8",
|
|
98
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
99
|
+
timeout: 100,
|
|
100
|
+
});
|
|
101
|
+
return themeFromMacOsAppearance(output);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// On macOS the key is absent in Light mode, and `defaults read` exits 1.
|
|
105
|
+
return "light";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function themeFromMacOsAppearance(output) {
|
|
109
|
+
return output?.trim().toLowerCase() === "dark" ? "dark" : "light";
|
|
110
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ImageDisplayMessage {
|
|
2
|
+
content?: string | null;
|
|
3
|
+
}
|
|
4
|
+
export declare function imageDisplayLabel(index: number): string;
|
|
5
|
+
export declare function imageDisplayLabels(count: number, labelStart?: number): string[];
|
|
6
|
+
export declare function imageDisplayReferenceLine(label: string): string;
|
|
7
|
+
export declare function isImageDisplayReferenceLine(line: string): boolean;
|
|
8
|
+
export declare function splitImageDisplayContent(content: string): {
|
|
9
|
+
bodyLines: string[];
|
|
10
|
+
referenceLines: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function formatImageUserDisplayText(input: string, imageCount: number, labelStart?: number): string;
|
|
13
|
+
export declare function nextImageDisplayLabelStart(messages: Iterable<ImageDisplayMessage>): number;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function imageDisplayLabel(index) {
|
|
2
|
+
return `[Image #${index}]`;
|
|
3
|
+
}
|
|
4
|
+
export function imageDisplayLabels(count, labelStart = 1) {
|
|
5
|
+
return Array.from({ length: Math.max(0, count) }, (_, index) => imageDisplayLabel(labelStart + index));
|
|
6
|
+
}
|
|
7
|
+
export function imageDisplayReferenceLine(label) {
|
|
8
|
+
return `└ ${label}`;
|
|
9
|
+
}
|
|
10
|
+
export function isImageDisplayReferenceLine(line) {
|
|
11
|
+
return /^└ \[Image #\d+\]$/.test(line.trimEnd());
|
|
12
|
+
}
|
|
13
|
+
export function splitImageDisplayContent(content) {
|
|
14
|
+
const bodyLines = [];
|
|
15
|
+
const referenceLines = [];
|
|
16
|
+
for (const line of content.split("\n")) {
|
|
17
|
+
if (isImageDisplayReferenceLine(line)) {
|
|
18
|
+
referenceLines.push(line);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
bodyLines.push(line);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { bodyLines, referenceLines };
|
|
25
|
+
}
|
|
26
|
+
export function formatImageUserDisplayText(input, imageCount, labelStart = 1) {
|
|
27
|
+
if (imageCount <= 0)
|
|
28
|
+
return input;
|
|
29
|
+
const labels = imageDisplayLabels(imageCount, labelStart);
|
|
30
|
+
const base = input.trim();
|
|
31
|
+
const headline = base ? `${labels.join(" ")} ${base}` : labels.join(" ");
|
|
32
|
+
return [
|
|
33
|
+
headline,
|
|
34
|
+
...labels.map(imageDisplayReferenceLine),
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
export function nextImageDisplayLabelStart(messages) {
|
|
38
|
+
let max = 0;
|
|
39
|
+
const pattern = /\[Image #(\d+)\]/g;
|
|
40
|
+
for (const message of messages) {
|
|
41
|
+
const content = message.content ?? "";
|
|
42
|
+
for (const match of content.matchAll(pattern)) {
|
|
43
|
+
const value = Number(match[1]);
|
|
44
|
+
if (Number.isFinite(value))
|
|
45
|
+
max = Math.max(max, value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return max + 1;
|
|
49
|
+
}
|
|
@@ -1,16 +1,47 @@
|
|
|
1
|
+
export interface HistoryScope {
|
|
2
|
+
sessionFile?: string | null;
|
|
3
|
+
cwd?: string | null;
|
|
4
|
+
}
|
|
5
|
+
export interface HistoryLoadOptions {
|
|
6
|
+
filePath?: string;
|
|
7
|
+
scope?: HistoryScope;
|
|
8
|
+
includeLegacy?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface HistoryAppendOptions {
|
|
11
|
+
filePath?: string;
|
|
12
|
+
scope?: HistoryScope;
|
|
13
|
+
createdAt?: Date | string;
|
|
14
|
+
}
|
|
15
|
+
export interface HistoryImageAttachment {
|
|
16
|
+
mediaType: string;
|
|
17
|
+
bytes: number;
|
|
18
|
+
dataUrl: string;
|
|
19
|
+
base64: string;
|
|
20
|
+
filename?: string;
|
|
21
|
+
sourcePath?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface HistoryEntry {
|
|
24
|
+
text: string;
|
|
25
|
+
images: HistoryImageAttachment[];
|
|
26
|
+
imageDisplayStart?: number;
|
|
27
|
+
}
|
|
1
28
|
export declare function defaultHistoryFilePath(): string;
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function
|
|
29
|
+
export declare function loadHistoryEntriesSync(arg?: string | HistoryLoadOptions): HistoryEntry[];
|
|
30
|
+
export declare function loadHistorySync(arg?: string | HistoryLoadOptions): string[];
|
|
31
|
+
export declare function appendHistoryEntry(entry: string | HistoryEntry, arg?: string | HistoryAppendOptions): void;
|
|
4
32
|
export interface HistoryNavState {
|
|
5
|
-
history: string
|
|
33
|
+
history: Array<string | HistoryEntry>;
|
|
6
34
|
index: number | null;
|
|
7
|
-
draft: string;
|
|
35
|
+
draft: string | HistoryEntry;
|
|
8
36
|
}
|
|
9
37
|
export interface HistoryNavResult {
|
|
10
38
|
text: string;
|
|
39
|
+
images?: HistoryImageAttachment[];
|
|
40
|
+
imageDisplayStart?: number;
|
|
11
41
|
index: number | null;
|
|
12
|
-
draft: string;
|
|
42
|
+
draft: string | HistoryEntry;
|
|
13
43
|
changed: boolean;
|
|
14
44
|
}
|
|
15
|
-
export declare function stepHistory(state: HistoryNavState, direction: "up" | "down",
|
|
45
|
+
export declare function stepHistory(state: HistoryNavState, direction: "up" | "down", currentEntry: string | HistoryEntry): HistoryNavResult;
|
|
16
46
|
export declare function pushHistoryEntry(history: string[], entry: string): string[];
|
|
47
|
+
export declare function pushHistoryEntry(history: HistoryEntry[], entry: HistoryEntry): HistoryEntry[];
|