From be89a604f1b0b183ffa6faf1bd693c58a803c978 Mon Sep 17 00:00:00 2001 From: Kabochar Date: Sat, 1 Aug 2026 15:07:55 +0800 Subject: [PATCH] feat: collapse slash command expansion in user messages Show a compact clickable command reference for slash-command messages (/skill:name and prompt templates) instead of the full SDK-expanded text. The user-supplied arguments stay fully visible; clicking the command name toggles the expanded skill/template body. - lib/slash-display.ts: pure helpers (command parsing, skill expansion reverse-matching, FIFO tracker) + 17 unit tests - rpc-manager.ts: inject originalText on message_end for user messages via a new message object (never mutates SDK-shared state), always consume one FIFO entry per message, clear the tracker on prompt failures - session-reader.ts: reverse-match skill expansions when loading session history - MessageView.tsx: render command name as accent-colored toggle with args shown verbatim; keep image blocks in both branches; add aria-expanded Storage and model input stay unchanged: session JSONL keeps the expanded text and originalText is a display-only field. --- components/MessageView.tsx | 130 ++++++++++++++++++++++++------ lib/rpc-manager.test.mjs | 27 +++++++ lib/rpc-manager.ts | 60 +++++++++++++- lib/session-reader.ts | 17 +++- lib/slash-display.test.mjs | 158 +++++++++++++++++++++++++++++++++++++ lib/slash-display.ts | 110 ++++++++++++++++++++++++++ lib/types.ts | 6 ++ 7 files changed, 479 insertions(+), 29 deletions(-) create mode 100644 lib/slash-display.test.mjs create mode 100644 lib/slash-display.ts diff --git a/components/MessageView.tsx b/components/MessageView.tsx index d73bee37f..11adf25ec 100644 --- a/components/MessageView.tsx +++ b/components/MessageView.tsx @@ -151,6 +151,14 @@ function UserMessageView({ message, cwd, onOpenFile, entryId, onFork, forking, o const { t } = useI18n(); const [hovered, setHovered] = useState(false); const [copied, setCopied] = useState(false); + // 斜杠命令消息:默认折叠为命令 chip,点击展开 SDK 展开后的完整文本 + const [expanded, setExpanded] = useState(false); + const originalText = message.originalText; + // 拆分命令名与参数:/skill:web-access 验证参数 → 命令名 + 参数(无空格则为纯命令) + const rawCommand = originalText ?? ""; + const spaceIndex = rawCommand.indexOf(" "); + const commandName = spaceIndex === -1 ? rawCommand : rawCommand.slice(0, spaceIndex); + const commandArgs = spaceIndex === -1 ? "" : rawCommand.slice(spaceIndex + 1).trim(); const content = typeof message.content === "string" @@ -167,10 +175,39 @@ function UserMessageView({ message, cwd, onOpenFile, entryId, onFork, forking, o const time = formatTime(message.timestamp); const canFork = !!entryId && !!onFork; + // 复制/编辑使用命令原文(界面所见即所得),展开全文仅用于展示 + const copyTarget = originalText ?? content; + + // 图片渲染(originalText 折叠分支与原分支共用,避免斜杠命令+图片时图片消失) + const imageBlocksNode = imageBlocks.length > 0 && ( +
+ {imageBlocks.map((img, i) => { + // lib/types.ts ImageContent uses {source:{type,data,media_type,url}} + // pi-ai on-disk format uses flat {data, mimeType} — handle both + const flat = img as unknown as { data?: string; mimeType?: string }; + const src = img.source + ? img.source.type === "base64" + ? `data:${img.source.media_type};base64,${img.source.data}` + : img.source.url ?? "" + : flat.data + ? `data:${flat.mimeType};base64,${flat.data}` + : ""; + return ( + // eslint-disable-next-line @next/next/no-img-element + + ); + })} +
+ ); const canNavigate = !!prevAssistantEntryId && !!onNavigate; const copyContent = () => { - copyText(content).then(() => { + copyText(copyTarget).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); @@ -197,32 +234,73 @@ function UserMessageView({ message, cwd, onOpenFile, entryId, onFork, forking, o wordBreak: "break-word", }} > - {imageBlocks.length > 0 && ( -
- {imageBlocks.map((img, i) => { - // lib/types.ts ImageContent uses {source:{type,data,media_type,url}} - // pi-ai on-disk format uses flat {data, mimeType} — handle both - const flat = img as unknown as { data?: string; mimeType?: string }; - const src = img.source - ? img.source.type === "base64" - ? `data:${img.source.media_type};base64,${img.source.data}` - : img.source.url ?? "" - : flat.data - ? `data:${flat.mimeType};base64,${flat.data}` - : ""; - return ( - // eslint-disable-next-line @next/next/no-img-element - - ); - })} + {originalText ? ( + // 斜杠命令消息:命令名紧凑展示(accent 色,点击展开/折叠全文), + // 用户输入的参数部分原文完整展示,不折叠;无 hover 背景反馈 +
+ {imageBlocksNode} +
+ + {commandArgs && ( + + {commandArgs} + + )} +
+ {expanded && ( + {content} + )}
- )} + ) : ( + <> + {imageBlocksNode} {content && {content}} + + )}
@@ -278,7 +356,7 @@ function UserMessageView({ message, cwd, onOpenFile, entryId, onFork, forking, o }}> {canNavigate && (