From aaf3917c71c1d86c4f5473589c6242fb49c0a6d1 Mon Sep 17 00:00:00 2001 From: Test User Date: Fri, 12 Jun 2026 20:41:59 +0800 Subject: [PATCH 1/2] fix(tui): hide plugin-injected system reminders from transcript Plugin hooks can inject system messages for the model (e.g. the subagent delegation reminder). These should not be rendered as visible SYSTEM entries in the TUI transcript because they repeat every turn and clutter the conversation. - Add optional flag to , , and . - Preserve through PluginManager, AgentLoop, ConversationMemory, and the OpenTUI bridge. - Filter hidden entries out of TUI transcript rendering and clipboard export. - Mark as hidden in subagent-plugin. --- packages/core/src/agent/agent-loop.ts | 2 +- .../src/agent/conversation-memory.test.ts | 18 +++++++++++ .../core/src/agent/conversation-memory.ts | 9 ++++-- packages/core/src/plugins/manager.ts | 1 + packages/core/src/plugins/types.ts | 2 ++ packages/protocol/src/index.ts | 2 ++ skills/builtin/src/plan-plugin.ts | 1 + skills/builtin/src/skill-plugin.ts | 1 + skills/builtin/src/subagent-plugin.ts | 1 + src/runtime/local-opentui-bridge.ts | 3 +- src/tui/app.tsx | 32 ++++++++++--------- src/tui/transcript-export.ts | 1 + src/tui/types.ts | 2 ++ 13 files changed, 56 insertions(+), 19 deletions(-) diff --git a/packages/core/src/agent/agent-loop.ts b/packages/core/src/agent/agent-loop.ts index 6c62d3bf..b9206fc3 100644 --- a/packages/core/src/agent/agent-loop.ts +++ b/packages/core/src/agent/agent-loop.ts @@ -946,7 +946,7 @@ export class AgentLoop { if (message.role === "user") { this.memory.addUser(content); } else { - this.memory.addSystem(content); + this.memory.addSystem(content, { hidden: message.hidden }); } } diff --git a/packages/core/src/agent/conversation-memory.test.ts b/packages/core/src/agent/conversation-memory.test.ts index abf4f3f1..757070e2 100644 --- a/packages/core/src/agent/conversation-memory.test.ts +++ b/packages/core/src/agent/conversation-memory.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect } from "vitest"; +import type { SystemMessage } from "@step-cli/protocol"; import { ConversationMemory, type MemoryConfig, @@ -100,6 +101,23 @@ describe("ConversationMemory", () => { expect(state.messages).toHaveLength(1); expect(state.messages[0]!.role).toBe("system"); }); + + it("preserves hidden flag through export/load round-trip", () => { + const memory = new ConversationMemory(makeConfig()); + memory.addSystem("hidden instruction", { hidden: true }); + memory.addSystem("visible instruction"); + + const exported = memory.exportState(); + const memory2 = new ConversationMemory(makeConfig()); + memory2.loadState(exported); + + const restored = memory2.exportState(); + expect(restored.messages).toHaveLength(2); + expect(restored.messages[0]!.role).toBe("system"); + expect((restored.messages[0]! as SystemMessage).hidden).toBe(true); + expect(restored.messages[1]!.role).toBe("system"); + expect((restored.messages[1]! as SystemMessage).hidden).toBeUndefined(); + }); }); describe("recordDecision", () => { diff --git a/packages/core/src/agent/conversation-memory.ts b/packages/core/src/agent/conversation-memory.ts index bd0f2849..a070b4c4 100644 --- a/packages/core/src/agent/conversation-memory.ts +++ b/packages/core/src/agent/conversation-memory.ts @@ -356,8 +356,12 @@ export class ConversationMemory { this.invalidateContextAssemblyForTranscriptMutation(); } - addSystem(content: string): void { - this.messages.push({ role: "system", content }); + addSystem(content: string, options?: { hidden?: boolean }): void { + this.messages.push({ + role: "system", + content, + ...(options?.hidden ? { hidden: true } : undefined), + }); this.invalidateContextAssemblyForTranscriptMutation(); const normalized = shortenLine(content, this.config.decisionEntryMaxChars); if (normalized.length > 0) { @@ -2048,6 +2052,7 @@ function cloneChatMessage(message: ChatMessage): ChatMessage { return { role: "system", content: message.content, + ...(message.hidden ? { hidden: true } : undefined), }; } diff --git a/packages/core/src/plugins/manager.ts b/packages/core/src/plugins/manager.ts index 448ae11b..a5e47f9d 100644 --- a/packages/core/src/plugins/manager.ts +++ b/packages/core/src/plugins/manager.ts @@ -211,6 +211,7 @@ export class PluginManager { injected.push({ role: message.role, content: truncated.text, + ...(message.hidden ? { hidden: true } : undefined), }); } diff --git a/packages/core/src/plugins/types.ts b/packages/core/src/plugins/types.ts index 03782f84..1acead0b 100644 --- a/packages/core/src/plugins/types.ts +++ b/packages/core/src/plugins/types.ts @@ -33,6 +33,8 @@ export interface PluginUserMessage { export type PluginInjectedMessage = { role: "system" | "user"; content: string; + /** When true, the message is only for the model context and should not be rendered in the UI. */ + hidden?: boolean; }; export interface PluginHookContext { diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index b1216184..888fa1c2 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -39,6 +39,8 @@ export interface JsonSchema { export interface SystemMessage { role: "system"; content: string; + /** Internal messages injected by plugins; should not be rendered in the UI. */ + hidden?: boolean; } export type UserAttachmentSource = diff --git a/skills/builtin/src/plan-plugin.ts b/skills/builtin/src/plan-plugin.ts index 46e223cc..5b71e4d5 100644 --- a/skills/builtin/src/plan-plugin.ts +++ b/skills/builtin/src/plan-plugin.ts @@ -135,6 +135,7 @@ export function createPlanPlugin(manager: PlanManager): ToolPlugin { { role: "system", content: renderPlanInjectedMessage(snapshot), + hidden: true, }, ], }; diff --git a/skills/builtin/src/skill-plugin.ts b/skills/builtin/src/skill-plugin.ts index 7527a30b..5b6ad9a8 100644 --- a/skills/builtin/src/skill-plugin.ts +++ b/skills/builtin/src/skill-plugin.ts @@ -103,6 +103,7 @@ export function createSkillPlugin( { role: "system" as const, content: renderInjectedSkills(injectedContents), + hidden: true, }, ] : undefined; diff --git a/skills/builtin/src/subagent-plugin.ts b/skills/builtin/src/subagent-plugin.ts index d5d6369f..def6af87 100644 --- a/skills/builtin/src/subagent-plugin.ts +++ b/skills/builtin/src/subagent-plugin.ts @@ -1268,6 +1268,7 @@ class BackgroundSubtaskManager { messages.push({ role: "system", content: MAIN_ORCHESTRATION_REMINDER, + hidden: true, }); } diff --git a/src/runtime/local-opentui-bridge.ts b/src/runtime/local-opentui-bridge.ts index 3400b6b7..6374be43 100644 --- a/src/runtime/local-opentui-bridge.ts +++ b/src/runtime/local-opentui-bridge.ts @@ -90,7 +90,7 @@ export class LocalOpenTuiTranscriptBridge implements StepCliTuiTranscriptControl return [ ...this.sessionEntries, ...this.localEntries.map(stripLocalTranscriptEntry), - ]; + ].filter((entry) => !entry.hidden); } subscribe( @@ -900,6 +900,7 @@ function mapChatMessageToTranscriptEntry( role: "system", caption: null, content: message.content, + hidden: message.hidden, }; } } diff --git a/src/tui/app.tsx b/src/tui/app.tsx index 2d7a5347..82880d8f 100644 --- a/src/tui/app.tsx +++ b/src/tui/app.tsx @@ -1914,21 +1914,23 @@ function buildTranscriptItems( ): TranscriptItem[] { return [ buildWelcomeTranscriptItem(width), - ...entries.map((entry, index) => { - const identity = resolveTranscriptIdentity(entry); - const body = compactToolTranscriptContent(entry); - const lines = wrapMultiline(body, Math.max(12, width - 4)); - return { - id: - entry.id || - `message:${index}:${identity.badge}:${identity.caption ?? ""}`, - ...identity, - backgroundColor: resolveTranscriptBackground(entry, theme), - border: false, - lines, - truncated: false, - }; - }), + ...entries + .filter((entry) => !entry.hidden) + .map((entry, index) => { + const identity = resolveTranscriptIdentity(entry); + const body = compactToolTranscriptContent(entry); + const lines = wrapMultiline(body, Math.max(12, width - 4)); + return { + id: + entry.id || + `message:${index}:${identity.badge}:${identity.caption ?? ""}`, + ...identity, + backgroundColor: resolveTranscriptBackground(entry, theme), + border: false, + lines, + truncated: false, + }; + }), ]; } diff --git a/src/tui/transcript-export.ts b/src/tui/transcript-export.ts index 085acd5f..71d2237a 100644 --- a/src/tui/transcript-export.ts +++ b/src/tui/transcript-export.ts @@ -4,6 +4,7 @@ export function buildTranscriptClipboardText( entries: readonly StepCliTuiTranscriptEntry[], ): string { return entries + .filter((entry) => !entry.hidden) .map((entry) => formatTranscriptClipboardBlock(entry)) .filter((block) => block.length > 0) .join("\n\n") diff --git a/src/tui/types.ts b/src/tui/types.ts index cebe79f1..adf838f9 100644 --- a/src/tui/types.ts +++ b/src/tui/types.ts @@ -87,6 +87,8 @@ export interface StepCliTuiTranscriptEntry { role: "assistant" | "user" | "tool" | "system"; content: string; caption: string | null; + /** Internal message that should not be rendered in the transcript. */ + hidden?: boolean; } export interface StepCliTuiQueuedTurnEntry { From 7a626a9b77ea84af483b5e1977f121277d183884 Mon Sep 17 00:00:00 2001 From: ZouR-Ma <2605315944@qq.com> Date: Wed, 15 Jul 2026 13:20:51 +0800 Subject: [PATCH 2/2] refactor(core): restrict hidden flag to system plugin messages PluginInjectedMessage allowed role "user" with a hidden flag that the agent loop silently ignored. Model it as a discriminated union so the flag only exists on the system variant, and document the constraint. --- packages/core/src/plugins/manager.ts | 14 +++++++++----- packages/core/src/plugins/types.ts | 21 +++++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/core/src/plugins/manager.ts b/packages/core/src/plugins/manager.ts index a5e47f9d..15e595e8 100644 --- a/packages/core/src/plugins/manager.ts +++ b/packages/core/src/plugins/manager.ts @@ -208,11 +208,15 @@ export class PluginManager { strategy: "head_tail", }); - injected.push({ - role: message.role, - content: truncated.text, - ...(message.hidden ? { hidden: true } : undefined), - }); + injected.push( + message.role === "system" + ? { + role: "system", + content: truncated.text, + ...(message.hidden ? { hidden: true } : undefined), + } + : { role: "user", content: truncated.text }, + ); } if (injected.length >= MAX_TOTAL_INJECTED_MESSAGES) { diff --git a/packages/core/src/plugins/types.ts b/packages/core/src/plugins/types.ts index 1acead0b..782d06ce 100644 --- a/packages/core/src/plugins/types.ts +++ b/packages/core/src/plugins/types.ts @@ -30,12 +30,21 @@ export interface PluginUserMessage { content: string; } -export type PluginInjectedMessage = { - role: "system" | "user"; - content: string; - /** When true, the message is only for the model context and should not be rendered in the UI. */ - hidden?: boolean; -}; +export type PluginInjectedMessage = + | { + role: "system"; + content: string; + /** + * When true, the message is only for the model context and should not + * be rendered in the UI. Only system messages support this flag; the + * agent loop stores injected user messages without it. + */ + hidden?: boolean; + } + | { + role: "user"; + content: string; + }; export interface PluginHookContext { workspaceRoot: string;