diff --git a/apps/app/src/app/components/part-view.tsx b/apps/app/src/app/components/part-view.tsx index 4105e0f99..6b15a4b1b 100644 --- a/apps/app/src/app/components/part-view.tsx +++ b/apps/app/src/app/components/part-view.tsx @@ -2,7 +2,7 @@ import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCle import { marked } from "marked"; import type { Part } from "@opencode-ai/sdk/v2/client"; import { File } from "lucide-solid"; -import { isTauriRuntime, safeStringify, summarizeStep } from "../utils"; +import { isTauriRuntime, isUserSkillIndicatorTextPart, safeStringify, summarizeStep } from "../utils"; import { usePlatform } from "../context/platform"; import { perfNow, recordPerfLog } from "../lib/perf-log"; @@ -573,6 +573,14 @@ export default function PartView(props: Props) { const textClass = () => (tone() === "dark" ? "text-gray-12" : "text-gray-12"); const subtleTextClass = () => (tone() === "dark" ? "text-gray-12/70" : "text-gray-11"); const panelBgClass = () => (tone() === "dark" ? "bg-gray-2/10" : "bg-gray-2/30"); + const primaryTextClass = createMemo(() => { + if (p().type !== "text" || !isUserSkillIndicatorTextPart(p())) { + return textClass(); + } + return tone() === "dark" + ? "italic text-[13px] leading-snug text-gray-11" + : "italic text-[13px] leading-snug text-gray-10"; + }); const toolOnly = () => true; const showToolOutput = () => developerMode(); const markdownSource = createMemo(() => { @@ -873,7 +881,7 @@ export default function PartView(props: Props) { ref={(el) => { textContainerEl = el; }} - class={`whitespace-pre-wrap break-words text-[14px] leading-relaxed max-h-[22rem] overflow-hidden ${textClass()}`.trim()} + class={`whitespace-pre-wrap break-words text-[14px] leading-relaxed max-h-[22rem] overflow-hidden ${primaryTextClass()}`.trim()} > {collapsedPreviewText()} @@ -899,7 +907,7 @@ export default function PartView(props: Props) { ref={(el) => { textContainerEl = el; }} - class={`whitespace-pre-wrap break-words ${textClass()}`.trim()} + class={`whitespace-pre-wrap break-words ${primaryTextClass()}`.trim()} > {renderTextWithLinks()} @@ -909,7 +917,7 @@ export default function PartView(props: Props) {
{ textContainerEl = el; }} - class={`whitespace-pre-wrap break-words ${textClass()}`.trim()} + class={`whitespace-pre-wrap break-words ${primaryTextClass()}`.trim()} > {renderTextWithLinks()}
@@ -917,7 +925,7 @@ export default function PartView(props: Props) {
{ textContainerEl = el; }} - class={`markdown-content max-w-none ${textClass()} + class={`markdown-content max-w-none ${primaryTextClass()} [&_strong]:font-semibold [&_em]:italic [&_h1]:text-2xl [&_h1]:font-bold [&_h1]:my-4 diff --git a/apps/app/src/app/components/session/message-list.tsx b/apps/app/src/app/components/session/message-list.tsx index 0644c97cd..5095e07fe 100644 --- a/apps/app/src/app/components/session/message-list.tsx +++ b/apps/app/src/app/components/session/message-list.tsx @@ -26,6 +26,7 @@ import { } from "../../types"; import { groupMessageParts, + isUserSkillIndicatorTextPart, isUserVisiblePart, summarizeStep, } from "../../utils"; @@ -380,9 +381,14 @@ export default function MessageList(props: MessageListProps) { props.expandedStepIds.has(id) || relatedIds.some((relatedId) => props.expandedStepIds.has(relatedId)); - const renderablePartsForMessage = (message: MessageWithParts) => - message.parts.filter((part) => { - if (!props.developerMode && !isUserVisiblePart(part)) { + const renderablePartsForMessage = (message: MessageWithParts) => { + const isUser = (message.info as { role?: string }).role === "user"; + return message.parts.filter((part) => { + const passesVisibility = + props.developerMode || + isUserVisiblePart(part) || + (isUser && isUserSkillIndicatorTextPart(part)); + if (!passesVisibility) { return false; } @@ -405,6 +411,7 @@ export default function MessageList(props: MessageListProps) { return props.developerMode; }); + }; const messageBlocks = createMemo(() => { const startedAt = perfNow(); @@ -463,7 +470,7 @@ export default function MessageList(props: MessageListProps) { const groupId = String((message.info as any).id ?? "message"); const attachments = attachmentsForParts(renderableParts); const nonAttachmentParts = renderableParts.filter((part) => !isAttachmentPart(part)); - const groups = groupMessageParts(nonAttachmentParts, groupId); + const groups = groupMessageParts(nonAttachmentParts, groupId, isUser); const isStepsOnly = groups.length > 0 && groups.every((group) => group.kind === "steps"); const stepGroups = isStepsOnly diff --git a/apps/app/src/app/pages/session.tsx b/apps/app/src/app/pages/session.tsx index ae4a6da49..b6fb09528 100644 --- a/apps/app/src/app/pages/session.tsx +++ b/apps/app/src/app/pages/session.tsx @@ -81,6 +81,7 @@ import type { import { buildOpenworkWorkspaceBaseUrl } from "../lib/openwork-server"; import { join } from "@tauri-apps/api/path"; import { + isUserSkillIndicatorTextPart, isUserVisiblePart, isTauriRuntime, isWindowsPlatform, @@ -519,8 +520,10 @@ export default function SessionView(props: SessionViewProps) { used += next.length; }; + const isUser = (message.info as { role?: string }).role === "user"; for (const part of message.parts) { - if (!isUserVisiblePart(part)) { + const includeSkillIndicator = isUser && isUserSkillIndicatorTextPart(part); + if (!isUserVisiblePart(part) && !includeSkillIndicator) { continue; } if (part.type === "text") { diff --git a/apps/app/src/app/utils/index.ts b/apps/app/src/app/utils/index.ts index afdebaddb..0d493f5c8 100644 --- a/apps/app/src/app/utils/index.ts +++ b/apps/app/src/app/utils/index.ts @@ -469,6 +469,17 @@ export function isVisibleTextPart(part: Part) { return part.type === "text" && isUserVisiblePart(part); } +/** Ignored companion text on user turns (e.g. slash-command skill label); not the synthetic template. */ +export function isUserSkillIndicatorTextPart(part: Part): boolean { + if (part.type !== "text") return false; + const flags = part as { synthetic?: boolean; ignored?: boolean }; + return flags.ignored === true && flags.synthetic !== true; +} + +export function isUserTranscriptTextPart(part: Part, isUserMessage: boolean): boolean { + return isVisibleTextPart(part) || (isUserMessage && isUserSkillIndicatorTextPart(part)); +} + const EXPLORATION_TOOL_NAMES = new Set(["read", "glob", "grep", "search", "list", "list_files"]); function isExplorationToolPart(part: Part) { @@ -477,7 +488,11 @@ function isExplorationToolPart(part: Part) { return EXPLORATION_TOOL_NAMES.has(tool); } -export function groupMessageParts(parts: Part[], messageId: string): MessageGroup[] { +export function groupMessageParts( + parts: Part[], + messageId: string, + isUserMessage = false, +): MessageGroup[] { const groups: MessageGroup[] = []; const explorationSteps: Part[] = []; let textBuffer = ""; @@ -514,10 +529,19 @@ export function groupMessageParts(parts: Part[], messageId: string): MessageGrou parts.forEach((part) => { if (part.type === "text") { - if (!isVisibleTextPart(part)) { + if (!isUserTranscriptTextPart(part, isUserMessage)) { return; } flushExplorationSteps(); + if (isUserSkillIndicatorTextPart(part)) { + flushText(); + groups.push({ + kind: "text", + part, + segment: sawExecution ? "result" : "intent", + }); + return; + } textBuffer += (part as { text?: string }).text ?? ""; return; }