mirror of
https://github.com/cline/cline.git
synced 2026-09-19 02:05:44 +08:00
* feat: add image generation support * fix(llms): preserve mixed image model behavior * fix(llms): validate generated image models * fix(llms): preserve mixed image response streaming * fix(llms): preserve runtime tool ownership * fix(llms): address image generation review feedback * fix(desktop): relay images for attached hub sessions * chore(llms): regenerate provider and model catalog Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vscode): preserve SDK model capabilities across the catalog boundary The new modelSupportsToolCalling gate treats a populated capability list without "tools" as authoritative. But the VS Code host round-trips model metadata through the legacy ModelInfo shape, and toSdkModelInfo reconstructed capability arrays from the legacy booleans alone — which have no "tools" projection. Every model with any capability flag set came back as "cannot call tools", so sessions registered zero tools and the file-edit e2e failed on all platforms (the editor tool call resolved to "Unknown tool" and the edit never reached disk). Fix, following the modalities-passthrough pattern so stacked capability PRs can reuse it: - Preserve the SDK capability list verbatim on legacy ModelInfo at the catalog boundary (adaptSdkModelInfo); union user overrides into it without ever fabricating a list from overrides alone. - Seed toSdkModelInfo from the preserved list, and when none survived, emit an explicit "tools" signal (honoring legacy supportsTools=false) so reconstructed arrays can never silently disable tool calling. - Add a shared modelHasCapability(model, capability, {assumeWhenUnspecified}) helper: missing or empty capability lists carry no signal and each check declares its own default. Future capability gates should route through it instead of reading model.capabilities directly. Verified: file-edit e2e (Single Root + Multi-Roots) passes locally; shared/core/llms/model-catalog/session-factory suites and typechecks pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(llms): refresh generated model catalog --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
195 lines
4.4 KiB
TypeScript
195 lines
4.4 KiB
TypeScript
import type {
|
|
AgentSideConnection,
|
|
SessionConfigOption,
|
|
SessionUpdate,
|
|
} from "@agentclientprotocol/sdk";
|
|
import type { AgentEvent } from "@cline/core";
|
|
import type { GeneratedMedia } from "@cline/shared";
|
|
import { getErrorMessage } from "@cline/shared";
|
|
import { buildToolTitle, mapToolKind } from "./tool-utils";
|
|
|
|
/**
|
|
* Maps an AgentEvent to zero or more ACP SessionUpdate notifications,
|
|
* sending each via the connection's sessionUpdate method.
|
|
*/
|
|
export function forwardAgentEvent(
|
|
conn: AgentSideConnection,
|
|
sessionId: string,
|
|
event: AgentEvent,
|
|
): void {
|
|
const updates = translateEvent(event);
|
|
for (const update of updates) {
|
|
void conn.sessionUpdate({ sessionId, update });
|
|
}
|
|
}
|
|
|
|
function translateEvent(event: AgentEvent): SessionUpdate[] {
|
|
switch (event.type) {
|
|
case "content_start":
|
|
return translateContentStart(event);
|
|
case "content_end":
|
|
return translateContentEnd(event);
|
|
case "done":
|
|
return [];
|
|
case "error":
|
|
return [];
|
|
case "iteration_start":
|
|
case "iteration_end":
|
|
case "usage":
|
|
return [];
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function translateContentStart(
|
|
event: AgentEvent & { type: "content_start" },
|
|
): SessionUpdate[] {
|
|
switch (event.contentType) {
|
|
case "text": {
|
|
if (!event.text) return [];
|
|
return [
|
|
{
|
|
sessionUpdate: "agent_message_chunk",
|
|
content: { type: "text", text: event.text },
|
|
},
|
|
];
|
|
}
|
|
case "reasoning": {
|
|
if (!event.reasoning) return [];
|
|
return [
|
|
{
|
|
sessionUpdate: "agent_thought_chunk",
|
|
content: { type: "text", text: event.reasoning },
|
|
},
|
|
];
|
|
}
|
|
case "tool": {
|
|
const toolCallId = event.toolCallId ?? "unknown";
|
|
const toolName = event.toolName ?? "unknown";
|
|
return [
|
|
{
|
|
sessionUpdate: "tool_call",
|
|
toolCallId,
|
|
title: buildToolTitle(toolName, event.input),
|
|
kind: mapToolKind(toolName),
|
|
status: "pending",
|
|
rawInput: event.input,
|
|
},
|
|
];
|
|
}
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function describeAgentError(error: unknown): string {
|
|
const message = getErrorMessage(error).trim();
|
|
return message || "The agent reported an unknown error.";
|
|
}
|
|
|
|
function translateContentEnd(
|
|
event: AgentEvent & { type: "content_end" },
|
|
): SessionUpdate[] {
|
|
const e = event as {
|
|
type: "content_end";
|
|
contentType: string;
|
|
text?: string;
|
|
reasoning?: string;
|
|
toolName?: string;
|
|
toolCallId?: string;
|
|
output?: unknown;
|
|
error?: string;
|
|
durationMs?: number;
|
|
media?: GeneratedMedia;
|
|
};
|
|
|
|
switch (e.contentType) {
|
|
case "text":
|
|
// Text was already streamed via content_start chunks; don't re-send.
|
|
return [];
|
|
case "reasoning":
|
|
// Reasoning was already streamed via content_start chunks; don't re-send.
|
|
return [];
|
|
case "media":
|
|
if (!e.media) return [];
|
|
if (e.media.modality !== "image" || e.media.source.type !== "base64") {
|
|
return [
|
|
{
|
|
sessionUpdate: "agent_message_chunk",
|
|
content: {
|
|
type: "text",
|
|
text: `[Generated ${e.media.modality}: ${e.media.mediaType}]`,
|
|
},
|
|
},
|
|
];
|
|
}
|
|
return [
|
|
{
|
|
sessionUpdate: "agent_message_chunk",
|
|
content: {
|
|
type: "image",
|
|
data: e.media.source.data,
|
|
mimeType: e.media.mediaType,
|
|
},
|
|
},
|
|
];
|
|
case "tool": {
|
|
const toolCallId = e.toolCallId ?? "unknown";
|
|
const failed = !!e.error;
|
|
return [
|
|
{
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
status: failed ? "failed" : "completed",
|
|
rawOutput: e.error ?? e.output,
|
|
},
|
|
];
|
|
}
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a current_mode_update notification to the client.
|
|
*/
|
|
export function sendCurrentModeUpdate(
|
|
conn: AgentSideConnection,
|
|
sessionId: string,
|
|
modeId: string,
|
|
): void {
|
|
void conn.sessionUpdate({
|
|
sessionId,
|
|
update: { sessionUpdate: "current_mode_update", currentModeId: modeId },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send a config_option_update notification to the client.
|
|
*/
|
|
export function sendConfigOptionUpdate(
|
|
conn: AgentSideConnection,
|
|
sessionId: string,
|
|
configOptions: Array<SessionConfigOption>,
|
|
): void {
|
|
void conn.sessionUpdate({
|
|
sessionId,
|
|
update: { sessionUpdate: "config_option_update", configOptions },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send a session_info_update notification to the client.
|
|
*/
|
|
export function sendSessionInfoUpdate(
|
|
conn: AgentSideConnection,
|
|
sessionId: string,
|
|
info: { title?: string | null; updatedAt?: string | null },
|
|
): void {
|
|
void conn.sessionUpdate({
|
|
sessionId,
|
|
update: { sessionUpdate: "session_info_update", ...info },
|
|
});
|
|
}
|