Files
cline/apps/cli/src/acp/session-updates.ts
T
BeeandClaude Fable 5 8bbdde2a5c feat(llms): add model-driven image generation (#13025)
* 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>
2026-08-14 12:58:53 -07:00

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 },
});
}