feat(core,server,web): images reach every input — steering, goal objectives, one fold (#71)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rank-YuandClaude Fable 5 authored and GitHub committed 2026-07-30 16:50:35 +08:00
1 parent de6380f278
commit 97852f43b8
40 files changed
+1474 -254

No files matched your search

+38 -16
View File
@@ -203,12 +203,13 @@ export function MessageItem({ item, ctx }: { item: ChatItem; ctx: StreamRenderCo
// files come out of the same pass and collapse into one banner naming them.
const { text, images, files } = splitAttachments(skills ? skills.rest : afterScheduled);
// Every goal round reads like a normal user message: the body in a user bubble with
// the round notice beneath (the system IS re-sending the user's request each round).
// the round notice beneath (the system IS re-sending the user's request each round) —
// the objective's images included, since they ride it as path lines.
if (goalRound) {
return (
<>
{skills && <SkillsBanner names={skills.skills} />}
<GoalRoundBanner round={goalRound.round} objective={text} />
<GoalRoundBanner round={goalRound.round} objective={text} images={images} />
</>
);
}
@@ -218,7 +219,7 @@ export function MessageItem({ item, ctx }: { item: ChatItem; ctx: StreamRenderCo
{skills && <SkillsBanner names={skills.skills} />}
{/* Files uploaded with this message: named above the bubble, like the other
message-level notices — the bytes live in the session scratchpad, the model
opens them by path (goal mode never gets here: it rejects non-text input). */}
opens them by path (goal mode never gets here: it takes text and images only). */}
{files.length > 0 && <AttachedFilesBanner files={files} />}
{text && (
<div className="anim-msg group my-4 flex flex-col items-end">
@@ -253,32 +254,53 @@ export function MessageItem({ item, ctx }: { item: ChatItem; ctx: StreamRenderCo
</>
);
}
case "user_steering":
case "user_steering": {
// Mid-run steering ([user_steering]-wrapped user text delivered between turns): a
// compact right-aligned user-styled chip inside the running Task's flow — visually
// lighter than a full prompt bubble, since it doesn't start a new Task (the Trace page
// still shows the raw marker text as-is).
// Images sent with the message live inside the same chip: `item.images` on a vision
// model (delivered as image messages right behind the text), or restored from the
// [attached image: …] path lines without one — the same two shapes a user_text bubble
// handles, so both render identically here.
const { text: steerText, images: steerImages } = splitAttachments(item.text);
const shown = [...steerImages, ...(item.images ?? [])];
return (
<div className="anim-msg group my-2 flex flex-col items-end">
<div className="flex max-w-[88%] items-start gap-1.5 rounded-md border border-gray-200 bg-gray-100 px-3 py-1.5 md:max-w-[75%] dark:border-gray-700 dark:bg-gray-800">
<GlyphIcon
d={USER_STEERING_ICON}
className="mt-1 shrink-0 text-gray-400 dark:text-gray-500"
/>
<p className="wrap-anywhere whitespace-pre-wrap text-sm leading-relaxed text-gray-800 dark:text-gray-100">
<span className="mr-1.5 text-xs font-semibold text-gray-500 dark:text-gray-400">
{S.chat.userSteering}
</span>
{item.text}
</p>
<div className="flex max-w-[88%] flex-col gap-1.5 rounded-md border border-gray-200 bg-gray-100 px-3 py-1.5 md:max-w-[75%] dark:border-gray-700 dark:bg-gray-800">
<div className="flex items-start gap-1.5">
<GlyphIcon
d={USER_STEERING_ICON}
className="mt-1 shrink-0 text-gray-400 dark:text-gray-500"
/>
<p className="wrap-anywhere whitespace-pre-wrap text-sm leading-relaxed text-gray-800 dark:text-gray-100">
<span className="mr-1.5 text-xs font-semibold text-gray-500 dark:text-gray-400">
{S.chat.userSteering}
</span>
{steerText}
</p>
</div>
{shown.length > 0 && (
<div className="flex flex-wrap justify-end gap-1.5">
{shown.map((src, i) => (
<ZoomableImage
key={i}
src={src}
alt={S.chat.imageAlt}
className="max-h-28 max-w-full rounded-md"
/>
))}
</div>
)}
</div>
<MessageMeta
{...(item.atMs !== undefined ? { atMs: item.atMs } : {})}
text={item.text}
text={steerText}
align="right"
/>
</div>
);
}
case "user_image":
return (
<div className="anim-msg group my-4 flex flex-col items-end">